Compare commits

...

1381 Commits
atlas3 ... main

Author SHA1 Message Date
Phil
c762869b83
Migrate prod bot (#1052)
* Updated dev config to point to new Twitch dev bot hosting location.

* Updated prod config to point to new Twitch bot hosting location.
2022-10-14 16:41:06 +01:00
FRC
aaa09f49c0
New component to add arbitrary (but static) react embeds to posts programatically + midterms component (#1051) 2022-10-14 16:21:54 +01:00
FRC
4d214c01b4
Tipping in posts (#1045)
* Tipping in posts

* Rm itemType field
2022-10-14 13:05:07 +01:00
ingawei
0a70652667
fixed tip button bug (#1049)
* fixed tip button bug
2022-10-14 02:20:32 -05:00
mqp
b4162a0896 Auto-prettification 2022-10-14 06:49:23 +00:00
ingawei
2ecece02c3 Auto-remove unused imports 2022-10-14 06:48:40 +00:00
ingawei
4359ad0530
added cancelling tipping and lil coin (#1047)
* added cancelling tipping and lil coin
* forced timeout to fix weird toast bug
2022-10-14 01:47:51 -05:00
ingawei
3f8988bf27
Inga/colorful answer comments (#1040)
* changing answers in comments to match colors, no longer indenting those responses
2022-10-14 00:07:54 -05:00
Austin Chen
41a46aad9b Update stability-client to fix /dream 2022-10-13 21:44:22 -07:00
James Grugett
4097082c75
Dynamically choose outcome in Position tooltip (#1048) 2022-10-13 23:29:57 -05:00
mantikoros
58cd0e57bd track tournaments 2022-10-13 21:22:32 -05:00
Austin Chen
3a63503161 Fix @/% suggestion regression
See https://github.com/ueberdosis/tiptap/pull/3239
2022-10-13 19:06:03 -07:00
mantikoros
abd06f272b contract card: show traders instead of volume 2022-10-13 21:05:21 -05:00
Austin Chen
ab1c3020da Warn users about Dream breakage 2022-10-13 18:45:09 -07:00
Sinclair Chen
c044460a91
Don't break build on unused import (#1046)
* Fix lint warnings

* Let vercel build when unused import
2022-10-13 18:16:22 -07:00
mantikoros
7a6725ee77 contract card: less busy design 2022-10-13 19:56:16 -05:00
Austin Chen
823d1ddd4c Update stability-client 2022-10-13 17:01:05 -07:00
Austin Chen
7d490e0de1 Expand image on hover 2022-10-13 16:23:25 -07:00
Sinclair Chen
96d2255cb1 fix find and replace mistake 2022-10-13 15:15:45 -07:00
Sinclair Chen
4a139c5cc2 Fix limit prob styling 2022-10-13 14:55:48 -07:00
James Grugett
47eb8abed0 Don't include loans in email payout message 2022-10-13 16:05:40 -05:00
Sinclair Chen
903fcc83b3 Fix form-control styling 2022-10-13 13:20:04 -07:00
Austin Chen
0615bb2d4b
List ManifoldDream as a bot 2022-10-13 13:12:22 -07:00
mantikoros
3508c94634 tip button: fix tip color 2022-10-13 14:33:41 -05:00
mantikoros
29c0dfe3fe tip button: remove fill color 2022-10-13 14:27:47 -05:00
mantikoros
18a3b66164 dream label 2022-10-13 14:16:30 -05:00
James Grugett
9e4f41253f Filter out resolved markets from daily movers 2022-10-13 13:49:48 -05:00
Sinclair Chen
546b0231e7
Die Daisy (#1042)
* un-daisy labels

* un-daisy inputs

* un-daisy input groups

* fixup input

* un-daisy selects

* un-daisy slider

* Uninstall daisy. Migrate colors

* un-daisy tables

* fix input error styling

* lint
2022-10-13 11:23:42 -07:00
Phil
8bb44593f3
Updated dev config to point to new Twitch dev bot hosting location. (#1044) 2022-10-13 17:19:50 +01:00
Pico2x
2c2bc61788 Fix bug with parsing in abritrary react components 2022-10-13 16:56:35 +01:00
Pico2x
34c9dbb3e7 Revert "Revert "New implementation of market card embeddings (#1025)""
This reverts commit d6525bae9f.
2022-10-13 16:25:15 +01:00
Ian Philips
d6525bae9f Revert "New implementation of market card embeddings (#1025)"
This reverts commit 3fc53112b9.
2022-10-13 09:19:28 -06:00
Ian Philips
da32a756a8 Need not follow contract for tag notification 2022-10-13 08:41:33 -06:00
Ian Philips
fa476c78dd Handle numeric outcomes in movers 2022-10-13 08:18:16 -06:00
Ian Philips
e7ba7e715f default cursor on open answer 2022-10-13 08:04:01 -06:00
Ian Philips
9bf82c6082 Match colors on portfolio 2022-10-13 08:00:04 -06:00
Ian Philips
3e1876f0dc Add flaggedByUsernames to firestore rules 2022-10-13 07:58:14 -06:00
ingawei
5ba4a9dce7
Inga/tip button (#1043)
* added tip jar
* made market actions/comments and manalink buttons IconButtons
2022-10-13 01:53:26 -05:00
James Grugett
4e5b78f4ee Use in-memory store for home featured section data 2022-10-12 21:15:40 -05:00
James Grugett
bc6fab399e Make text grayer 2022-10-12 20:51:48 -05:00
James Grugett
c2d112e516 Position => shares 2022-10-12 20:51:20 -05:00
ingawei
3cbe8ad8bb
Inga/comment bounty fix (#1041)
* fixed bounty button in comments
2022-10-12 20:34:07 -05:00
ingawei
6226291e02 made comments smaller 2022-10-12 17:53:37 -07:00
Austin Chen
fa4dba4da3 Document the new /comment endpoint 2022-10-12 17:26:00 -07:00
Austin Chen
9eff69be75
Add /createcomment API endpoint (#946)
* /dream api: Upload StableDiffusion image to Firestore

* Minor tweaks

* Set content type on uploaded image

This makes it so the image doesn't auto-download when opened in a new tab

* Allow users to dream directly from within Manifold

* Remove unused import

* Implement a /comment endpoint which supports html and markdown

* Upgrade @tiptap/core to latest

* Update all tiptap deps to beta.199

* Add @tiptap/suggestion

* Import @tiptap/html in the right place

* ... add deps everywhere

So I have no idea how common deps work apparently

* Add tiptap/suggestion too

* Clean up dream

* More cleanups

* Rework /comment endpoint

* Move API to /comment

* Change imports in case that matters

* Add a couple todos

* Dynamically import micromark

* Parallellize gsutil with -m option

* Adding comments via api working, editor.tsx erroring out

* Unused import

* Remove disabled state from useTextEditor

Co-authored-by: Ian Philips <iansphilips@gmail.com>
2022-10-12 17:25:17 -07:00
James Grugett
789bec2a4f Expand replies by default (quick fix for comment links not working) 2022-10-12 17:49:04 -05:00
James Grugett
18042cd4d1 Return listener 2022-10-12 17:39:13 -05:00
mantikoros
04a126707b fix welcome dialog page freezing bug 2022-10-12 17:33:18 -05:00
mantikoros
7a412fdb0d add key props 2022-10-12 17:33:18 -05:00
James Grugett
e2dc4c6b8f Resolve markets again script 2022-10-12 17:30:21 -05:00
mantikoros
204d302d87 portfolio copy: "Open" => "Active" 2022-10-12 16:57:21 -05:00
James Grugett
ae39c1175b
Better resolve market payouts (#1038)
* Check payout preconditions first. Try to pay out market in 1 transaction.

* Format

* toBatch => lodash's chunk
2022-10-12 16:21:37 -05:00
Marshall Polaris
c44f223064
Fix some hydration issues (#1033)
* Extract `useIsClient` hook

* Fix a bunch of hydration bugs relevant to the contract page
2022-10-12 14:07:07 -07:00
mantikoros
aa717a767d backfill subsidyPool 2022-10-12 16:05:15 -05:00
Sinclair Chen
d9f57b7daa
Remove all daisy buttons (#1036)
* Tweak limit order UI and fix button

* Style all follow/unfollow buttons blue

also get rid of highlight-blue button

* remove all other uses of 'btn'

* Style group follow button like user follow

* lint and format
2022-10-12 12:27:17 -07:00
mantikoros
93ceaa52c4 payouts: fix subsidyPool undefined bug 2022-10-12 14:24:54 -05:00
James Grugett
de76557326 Show shares as contract card position. Fix bug on outcome 2022-10-12 13:55:58 -05:00
James Grugett
da1fcb646f Fix infinite re-render on home page 2022-10-12 13:42:54 -05:00
ingawei
1d618ba337
Inga/fr remove double comments (#1019)
incorporating answer comments into general comments section
2022-10-12 13:05:58 -05:00
Pico2x
2cda3a4d4f Don't show spinner if pinned items is undefined or zero 2022-10-12 18:12:32 +01:00
Sinclair Chen
e44fc8ae13
Simplify rich text to string parsing logic (#1022)
* Simplify rich text to string parsing logic

* lint
2022-10-12 10:09:59 -07:00
James Grugett
e6a90e18e4 Add more padding and improve layout of post card 2022-10-12 11:59:29 -05:00
Austin Chen
cee8caa3e8
Generate images from StableDiffusion (#1035)
* Generate images from StableDiffusion

* Update yarn.lock

* Log an error, remove extra comment

* Code cleanup

* Note about the API
2022-10-12 09:53:04 -07:00
Pico2x
b49264ddfa Show featured to all users 2022-10-12 17:44:43 +01:00
Pico2x
12ed569ff6 Update post card subtitle color 2022-10-12 17:33:02 +01:00
Pico2x
00acc262a0 Update post-cards to show profile pic correctly 2022-10-12 17:31:00 +01:00
Pico2x
fd7d4eb5e2 Make post-cards consistent with contract cards 2022-10-12 17:22:58 +01:00
Pico2x
8ae1166c49 Posts now have denormalized creator username and name 2022-10-12 16:42:28 +01:00
FRC
84e2b63c49
Entire homepage now loads simultaneously including featured (#1034) 2022-10-12 16:02:57 +01:00
FRCassarino
f19ef83ac2 Auto-remove unused imports 2022-10-12 14:38:59 +00:00
Pico2x
0c11f3b450 Fix is admin in featured 2022-10-12 15:38:06 +01:00
Pico2x
de9ffa2b52 Fix bug in which Featured section shows for everyone 2022-10-12 15:29:32 +01:00
Ian Philips
decb3213f6 Ignore cancel resolutions for proven correct 2022-10-12 08:05:55 -06:00
FRC
ff6278b147
Featured items to homepage (#1024)
* Featured items to homepage

* Fix nits
2022-10-12 15:04:39 +01:00
FRC
3fc53112b9
New implementation of market card embeddings (#1025)
* Grids of cards now implemented by rendering component instead of iframe

* Sinclair's nit
2022-10-12 13:24:22 +01:00
Marshall Polaris
59cdc9f776
Update FR colors, consolidate non-top answers into "Other" (#1031)
* Update FR colors, consolidate non-top answers into "Other"

* Fix answer panel coloration to not be weird and work on Firefox
2022-10-11 23:59:11 -07:00
ingawei
f587e0256d
standardizing red and green colors (#1032) 2022-10-12 01:58:20 -05:00
ingawei
1c209f68f6
de daisy sell button (#1030)
* de daisy sell button
2022-10-12 01:31:32 -05:00
ingawei
b4e7d88ed8
de daisied cancel limit bet button (#1029) 2022-10-12 01:13:43 -05:00
ingawei
b2cd6bbe03
Inga/de daisy follow button (#1028)
* de daisy follow button
2022-10-12 01:00:52 -05:00
ingawei
a6d5d5ad15
made create a post button not daisy (#1027)
yay no daisy
2022-10-12 00:24:59 -05:00
ingawei
beeca57d4e
getting rid of daisy for limit order button (#1026)
* getting rid of daisy for limit order button, got rid of betChoice in limit order panel
2022-10-12 00:09:45 -05:00
Ian Philips
fb8bd1acfb Handle slugs with and without leading / 2022-10-11 17:22:58 -06:00
Ian Philips
4215821f35 Fix firebase query for market creators 2022-10-11 16:59:47 -06:00
Ian Philips
a71c3d6a4a Fix notification link 2022-10-11 16:59:20 -06:00
James Grugett
cdcce421a8 Include today's bets in daily profit 2022-10-11 17:32:35 -05:00
Ian Philips
8beff6eb1f Format money 2022-10-11 15:36:35 -06:00
Ian Philips
f714918b88 Update functions readme with local dev details 2022-10-11 15:32:53 -06:00
James Grugett
946d74489f Gray scale position and profit 2022-10-11 16:29:15 -05:00
mantikoros
220d0841bd move liquidity to info dialog 2022-10-11 16:22:35 -05:00
James Grugett
9d44190b9a Fix group url nav to correct tabs 2022-10-11 16:04:40 -05:00
Sinclair Chen
3cdd790ae9
Autosave post, market, comment rich text (#1015)
* Fix freezing when typing big docs

* Make rich text fields autosave to localstorage

* Add autosave for comments

* delete vestigial text editor from challenges

* Clear autosave on submit post/market/comment

* lint
2022-10-11 12:52:27 -07:00
mantikoros
6c1ac89cbe typo 2022-10-11 14:28:10 -05:00
mantikoros
0d8a84ef06 re-order tournaments 2022-10-11 14:26:52 -05:00
James Grugett
d528566ffa People's=>Party #2 2022-10-11 13:46:58 -05:00
James Grugett
b0f8369d9c People's=>Party 2022-10-11 13:41:30 -05:00
James Grugett
721c18cf6c Add tournament for CCP 20th Congress 2022-10-11 13:37:40 -05:00
mantikoros
43b06ae6fa tip button: show number of tips 2022-10-11 13:30:30 -05:00
Ian Philips
bfdb5ae595 Cleanup comments 2022-10-11 10:26:37 -06:00
Ian Philips
274f7fa849 Send market closed notifications every 5 days 2022-10-11 10:22:38 -06:00
Ian Philips
d507c4092e Code for removing erroneous badges 2022-10-11 08:51:51 -06:00
Ian Philips
e970a908c6 Switch logic to includes 2022-10-11 08:35:59 -06:00
Pico2x
4fd0e5caad Fix bug in which new users are flagged as unreliable 2022-10-11 13:39:40 +01:00
James Grugett
70b2b14f80
Daily profit 💰 (#1023)
* Daily profit client side

* Filter out those where profit rounds to 0

* Tabs to spaces
2022-10-11 00:32:55 -05:00
mantikoros
0ec15ff2f8
Make liquidity great again (#1020)
* add subsidy

* drizzle liquidity

* update liquidity panel

* remove addliquidity

* update cloud functions index

* remove json endpoints

* imports

* drizzle liquidity: add velocity; dev script; run every minute

* adjust speed

* logging

* liquidity button, dialog

* modal size

* modal

* info table

* pay back excess liquidity

* remove client withdrawal

* house liquidity subsidy

* disable liquidity button if market resolved or closed

* format tip amount
2022-10-10 21:56:16 -05:00
Sinclair Chen
8bb9885aee
Fix @mention 500 error and can't close market bug (#1021)
* Fix @mention 500 error. Refactor text concat exts

* lint
2022-10-10 18:47:02 -07:00
Ian Philips
c46c384d1d Add more bot tags, better creator name scaling 2022-10-10 15:38:27 -06:00
Ian Philips
4f5c93be96 Badge notifications ui changes 2022-10-10 15:01:18 -06:00
James Grugett
f03e5d7af0
Refactor portfolio query (#1018)
* Fetch less data for portfolio query

* Rename var
2022-10-10 15:51:51 -05:00
Sinclair Chen
fb0a09664e delete bannerUrl from user type 2022-10-10 13:51:27 -07:00
Ian Philips
17d0fb7da6 Change badge award notif setting group 2022-10-10 14:41:24 -06:00
Ian Philips
867cdf2496 Only backfill unfilled users' badges 2022-10-10 14:34:06 -06:00
Ian Philips
f26ba1c4a2
Award badges for market creation, betting streaks, proven correct (#891)
* Award badges for market creation, betting streaks, proven correct

* Styling

* Add minimum unique bettors for proven correct

* Add name, refactor

* Add notifications for badge awards

* Correct styling

* Need at least 3 unique bettors for market maker badge

* Lint

* Switch to badges_awarded

* Don't include n/a resolutions in market creator badge

* Add badges by rarities to profile

* Show badges on profile, soon on market page

* Add achievements to new user

* Backfill all users badges
2022-10-10 14:32:29 -06:00
James Grugett
cdc64c6475 Correctly configure env var for firebase functions 2022-10-10 14:55:33 -05:00
James Grugett
5d561acdf8 Fix NaN 2022-10-10 14:23:16 -05:00
James Grugett
84f79ffe7c Remove undefined props 2022-10-10 13:34:02 -05:00
James Grugett
f6fd703005
Store each user's contract bet metrics (#1017)
* Implement most of caching metrics per user per contract

* Small group updates refactor

* Write contract-metrics subcollection

* Fix type error
2022-10-10 13:05:17 -05:00
mantikoros
b8ef272784 withdrawal warning 2022-10-10 13:01:57 -05:00
Ian Philips
a4699b79ed If unlisted during creation, fill in creator id 2022-10-10 10:48:01 -06:00
Ian Philips
66071e16fa Try without timeout seconds on pubsub 2022-10-10 09:53:42 -06:00
Ian Philips
b3136ebcac Update update-metrics timeout sends 2022-10-10 09:48:46 -06:00
Ian Philips
a143a96919 Fix unable to close contract 2022-10-10 09:24:37 -06:00
Ian Philips
dea65a4ba0 Better error handling for notification destinations 2022-10-10 07:41:41 -06:00
Ian Philips
a310963952 update prefs safely 2022-10-10 07:01:44 -06:00
Sinclair Chen
8d06e4b4d2
refactor text input into one component (#1016)
* Add responsive text input component

* Add styled expanding textarea component
2022-10-09 19:37:24 -07:00
James Grugett
dc51e2cf46 Rename updateMetrics to scheduleUpdateMetrics 2022-10-09 19:11:44 -05:00
sipec
4831c25ce0 Auto-prettification 2022-10-09 23:10:02 +00:00
Sinclair Chen
60f2552139 copy: Referrals -> Refer a friend 2022-10-09 16:09:21 -07:00
mantikoros
4b8d381da5 hide comment bounty when market closed or resolved 2022-10-09 17:14:20 -05:00
mantikoros
565177b76f track midterms, date docs 2022-10-09 17:02:34 -05:00
Sinclair Chen
8bd21c6693 hotfix %mention, add load-fail state 2022-10-08 22:52:36 -07:00
James Grugett
310a41d63e Make loan and bet streak links hoverable in notifications 2022-10-08 12:51:58 -05:00
mantikoros
e1636d0f13 update metrics: fix divide by zero, elasticity NaN bug 2022-10-08 12:16:45 -05:00
James Grugett
d00ea65279 Add MattP to winners for AMM liquidity exploit 2022-10-07 17:45:42 -05:00
James Grugett
60bb5379cb Update bounties doc 2022-10-07 17:30:20 -05:00
James Grugett
f3dedfb27a
Call updatemetrics v2 cloud function from scheduled function (#1014)
* Call updatemetrics v2 cloud function from scheduled function

* Set limits on bets and contracts loaded for portfolio page. Show warning if limit is hit

* mqp review: Use console.error if !response.ok
2022-10-07 17:10:12 -05:00
mantikoros
efa2e44937 comment bounty dialog, styling 2022-10-07 16:26:30 -05:00
mantikoros
84bc490ed3 comment sort: move below input, newest as default 2022-10-07 16:26:30 -05:00
James Grugett
443397b7dc
Action to merge main into main2 automatically 2022-10-07 15:13:57 -05:00
James Grugett
b57ff68654 Fix highlight & scroll to comment 2022-10-07 14:40:38 -05:00
Sinclair Chen
f0b35993c9 fix hydration error (button inside button) 2022-10-07 10:56:27 -07:00
James Grugett
8f56ccad22 Set limits on bets and contracts loaded for portfolio page. Show warning if limit is hit 2022-10-07 11:55:47 -05:00
mantikoros
9e289146af flat trade fee of M$0.1 aka bot tax 2022-10-06 23:04:48 -05:00
James Grugett
4285198f09 Merge branch 'main' into main2 2022-10-06 22:19:39 -05:00
James Grugett
f533d9bfcb
Verify balance of limit order "makers" (#1007)
* Fetch balance of users with open limit orders & cancel orders with insufficient balance

* Fix imports

* Fix bugs

* Fix a bug

* Remove redundant cast

* buttons overlaying content fix (#1005)

* buttons overlaying content fix

* stats: round DAU number

* made set width for portfolio/profit fields (#1006)

* tournaments: included resolved markets

* made delete red, moved button for regular posts (#1008)

* Fix localstorage saved user being overwritten on every page load

* Market page: Show no right panel while user loading

* Don't flash sign in button if user is loading

* election map coloring

* market group modal scroll fix (#1009)

* midterms: posititoning, make mobile friendly

* Un-daisy share buttons (#1010)

* Make embed and challenge buttons non-daisyui

* Allow link Buttons. Change tweet, dupe buttons.

* lint

* don't insert extra lines when upload photos

* Map fixes (#1011)

* usa map: fix sizing

* useSetIframeBackbroundColor

* preload contracts

* seo

* remove hook

* turn off sprig on dev

* Render timestamp only on client to prevent error of server not matching client

* Make sized container have default height so graph doesn't jump

* midterms: use null in static props

* Create common card component (#1012)

* Create common card component

* lint

* add key prop to pills

* redirect to /home after login

* create market: use transaction

* card: reduce border size

* Update groupContracts in db trigger

* Default sort to best

* Save comment sort per user rather than per contract

* Refactor Pinned Items into a reusable component

* Revert "create market: use transaction"

This reverts commit e1f24f24a9.

* Mark @v with a (Bot) label

* fix padding on daily movers

* fix type errors

* Wrap sprig init in check for window

* unindex date-docs from search engines

* Auto-prettification

* compute elasticity

* change dpm elasticity

* Fix google lighthouse issues (#1013)

* don't hide free response panel on open resolve

* liquidity sort

* Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'.

* Date doc: Toggle to disable creating a prediction market

* Listen for date doc changes

* Fix merge error

* Don't cancel all a users limit orders if they go negative

Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
Co-authored-by: mantikoros <95266179+mantikoros@users.noreply.github.com>
Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Pico2x <pico2x@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: sipec <sipec@users.noreply.github.com>
2022-10-06 22:16:49 -05:00
Austin Chen
71b0c71729 Tag ArbitrageBot with bot badge 2022-10-06 21:52:55 -05:00
mantikoros
25333317b0 Show elasticity; volume tooltip 2022-10-06 21:52:55 -05:00
Austin Chen
42a7d04b4d Tag ArbitrageBot with bot badge 2022-10-06 20:17:34 -04:00
James Grugett
b1d386ca5a Listen for date doc changes 2022-10-06 18:54:22 -05:00
James Grugett
0dc8753a92 Listen for date doc changes 2022-10-06 18:50:53 -05:00
mantikoros
454f2d1417 Merge branch 'main' into main2 2022-10-06 18:48:56 -05:00
James Grugett
d846b9fb30 Date doc: Toggle to disable creating a prediction market 2022-10-06 18:42:56 -05:00
James Grugett
77e0631ea4 Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'. 2022-10-06 18:42:56 -05:00
James Grugett
badd67c278 Date doc: Toggle to disable creating a prediction market 2022-10-06 18:36:27 -05:00
mantikoros
80622dc7ee liquidity sort 2022-10-06 18:23:27 -05:00
James Grugett
9d12fa3af0 Limit order trade log: '/' to 'of'. Remove 'of' in 'of YES'. 2022-10-06 18:03:44 -05:00
Sinclair Chen
d9c8925ea0 don't hide free response panel on open resolve 2022-10-06 15:20:46 -07:00
Sinclair Chen
adb809f973
Fix google lighthouse issues (#1013) 2022-10-06 15:19:37 -07:00
mantikoros
a63405ca7c change dpm elasticity 2022-10-06 16:47:52 -05:00
mantikoros
7ca0fb72fc compute elasticity 2022-10-06 16:36:32 -05:00
sipec
ac37f94cf7 Auto-prettification 2022-10-06 20:50:29 +00:00
Sinclair Chen
bc5af50b0c unindex date-docs from search engines 2022-10-06 13:49:39 -07:00
James Grugett
4162cca3ff Wrap sprig init in check for window 2022-10-06 15:23:51 -05:00
mantikoros
91da39370f fix type errors 2022-10-06 14:54:22 -05:00
Sinclair Chen
2f2c586d5d fix padding on daily movers 2022-10-06 12:01:00 -07:00
Austin Chen
853e3e4896 Mark @v with a (Bot) label 2022-10-06 14:20:35 -04:00
Pico2x
edbd0feb37 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-06 17:04:32 +01:00
Pico2x
59de979949 Refactor Pinned Items into a reusable component 2022-10-06 17:04:00 +01:00
mantikoros
b8d65acc3f Revert "create market: use transaction"
This reverts commit e1f24f24a9.
2022-10-06 10:54:42 -05:00
Ian Philips
26f04fb04a Save comment sort per user rather than per contract 2022-10-06 10:16:29 -04:00
Ian Philips
e127f9646a Default sort to best 2022-10-06 09:53:55 -04:00
Ian Philips
25ef17498a Update groupContracts in db trigger 2022-10-06 09:26:35 -04:00
mantikoros
68075db3da card: reduce border size 2022-10-05 22:20:51 -05:00
mantikoros
e1f24f24a9 create market: use transaction 2022-10-05 22:18:19 -05:00
mantikoros
cd8245fbee redirect to /home after login 2022-10-05 21:38:13 -05:00
mantikoros
f1e400765a add key prop to pills 2022-10-05 21:31:34 -05:00
Sinclair Chen
94624c5387
Create common card component (#1012)
* Create common card component

* lint
2022-10-05 18:02:24 -07:00
mantikoros
7ce09ae39d midterms: use null in static props 2022-10-05 19:24:03 -05:00
James Grugett
935bdd12a7 Make sized container have default height so graph doesn't jump 2022-10-05 18:44:30 -05:00
James Grugett
5d7721e041 Render timestamp only on client to prevent error of server not matching client 2022-10-05 18:44:30 -05:00
mantikoros
a149777c0e turn off sprig on dev 2022-10-05 18:36:32 -05:00
mantikoros
81fb2456bd remove hook 2022-10-05 18:29:08 -05:00
mantikoros
f8ec306ee9
Map fixes (#1011)
* usa map: fix sizing

* useSetIframeBackbroundColor

* preload contracts

* seo
2022-10-05 18:20:40 -05:00
Sinclair Chen
a53fb49ec3 don't insert extra lines when upload photos 2022-10-05 16:08:01 -07:00
Sinclair Chen
7863a4232d
Un-daisy share buttons (#1010)
* Make embed and challenge buttons non-daisyui

* Allow link Buttons. Change tweet, dupe buttons.

* lint
2022-10-05 15:51:10 -07:00
mantikoros
a3b841423f midterms: posititoning, make mobile friendly 2022-10-05 17:12:50 -05:00
ingawei
b8911cafe8
market group modal scroll fix (#1009) 2022-10-05 17:07:41 -05:00
mantikoros
60aa294131 election map coloring 2022-10-05 16:58:47 -05:00
James Grugett
0818a94307 Don't flash sign in button if user is loading 2022-10-05 16:56:47 -05:00
James Grugett
a3acd3fa3c Market page: Show no right panel while user loading 2022-10-05 16:52:16 -05:00
James Grugett
1ef1af8234 Fix localstorage saved user being overwritten on every page load 2022-10-05 16:49:28 -05:00
ingawei
189da4a0cf
made delete red, moved button for regular posts (#1008) 2022-10-05 16:21:03 -05:00
mantikoros
10f0bbc63d tournaments: included resolved markets 2022-10-05 15:46:20 -05:00
ingawei
2d56525d65
made set width for portfolio/profit fields (#1006) 2022-10-05 15:40:26 -05:00
mantikoros
f1f8082600 stats: round DAU number 2022-10-05 15:27:20 -05:00
ingawei
ec006f25c4
buttons overlaying content fix (#1005)
* buttons overlaying content fix
2022-10-05 15:19:10 -05:00
James Grugett
b40a114168 Print error from usePagination 2022-10-05 15:13:40 -05:00
Ian Philips
4bbadeb27c Parse images and iframes from tiptap to string descriptions 2022-10-05 14:08:51 -06:00
mantikoros
2596d54831 update tournaments page 2022-10-05 14:46:15 -05:00
Austin Chen
0df5497ffb Fix regression of unable to purchase mana 2022-10-05 15:35:02 -04:00
Ian Philips
27dabc193c Comment out logs 2022-10-05 13:09:40 -06:00
Ian Philips
6ec1b38a21 Add more stringent duplication req and popularity score 2022-10-05 13:07:23 -06:00
James Grugett
f35eb42d7b Make search query params work on page load 2022-10-05 13:27:46 -05:00
James Grugett
18f8ad433d Hide sort and query options when searching 2022-10-05 13:12:30 -05:00
Sinclair Chen
37e8f2ff5a
Make %mentions actually look like mentions (#993)
* Make contract mentions inline text

* Add tooltip for author, close time, volume

* Make pill wider
2022-10-05 11:11:05 -07:00
Ian Philips
328aa1457d Send interesting markets based on groups, follows, similar bettors 2022-10-05 10:41:57 -06:00
Austin Chen
b9ba3e75fa Show top 5k markets in sitemap 2022-10-05 11:49:47 -04:00
Austin Chen
70bfec2742 Improve sitemaps 2022-10-05 11:41:32 -04:00
Pico2x
26281556f7 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-05 15:54:13 +01:00
Austin Chen
730abf584a Revert "Warn whenever rich text editor has unsaved changes"
This reverts commit 419219c703.
2022-10-05 10:54:04 -04:00
Pico2x
34d09316e0 Yscale now updates when zooming in on chart 2022-10-05 15:46:18 +01:00
Austin Chen
6f41ab8efd Keep unlisted state on duplicate 2022-10-05 10:38:23 -04:00
Pico2x
f1207e87ec Update share-modal.tsx 2022-10-05 15:18:53 +01:00
Pico2x
4e22b8e332 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-05 15:01:44 +01:00
Pico2x
07de8cc86a Slightly less horrible color palette 2022-10-05 15:01:41 +01:00
akrolsmir
f07a022d63 Auto-remove unused imports 2022-10-05 13:58:54 +00:00
Austin Chen
d42ec42b0e Standardize on CopyLinkButton 2022-10-05 09:55:46 -04:00
Austin Chen
6fa4e17a58 Remove "Add my comment" button for signed out users 2022-10-05 09:35:21 -04:00
Austin Chen
af3a3a3934 Clean up style on Get M$ 2022-10-05 09:30:51 -04:00
Pico2x
9e3477970d Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-05 14:19:38 +01:00
Pico2x
3390c34d0a Mobile tooltip isn't occluded by finger anymore 2022-10-05 14:19:26 +01:00
Austin Chen
419219c703 Warn whenever rich text editor has unsaved changes 2022-10-05 09:16:05 -04:00
Pico2x
8aaca848b2 Updating group name works again. 2022-10-05 14:02:17 +01:00
Pico2x
e4d7d0a232 Mobile graphs no longer show the zoom function. 2022-10-05 13:57:45 +01:00
Pico2x
e9050973e1 Update post-comments.tsx 2022-10-05 11:53:59 +01:00
FRC
83d9a1f3e2
Posts changes (#988)
* Add post subtitle

* Add "Post" badge to post card

* Move post tab to overview tab, refactor components

* Fix styling nits.
2022-10-05 11:37:23 +01:00
Marshall Polaris
49e97ddac1
Small add-on React 18 fixes (#1004)
* Bump React types for main code to 18

* Replace react-beautiful-dnd with maintained fork

* Add a type annotation
2022-10-05 00:23:23 -07:00
Marshall Polaris
a9d5dd7fc8
Memoize FeedBet component (#999) 2022-10-04 23:17:09 -07:00
Marshall Polaris
ddb186dd98
Change Tipper interface, memoize FeedComment (#1000)
* Change `Tipper` interface, memoize `FeedComment`

* Fixup per James feedback

* More fixup per James feedback
2022-10-04 23:16:56 -07:00
Marshall Polaris
d2273087cf
React 17.0.2 -> 18.2.0 (#1003)
* React 17.0.2 -> 18.2.0

* Adjust title tag to only have one text node (no internal spaces)

* react-expanding-textarea 2.3.5 -> 2.3.6
2022-10-04 23:16:39 -07:00
James Grugett
6a0b577aeb Small home refactor 2022-10-05 00:54:38 -05:00
James Grugett
ca6197c7bb Show total loan amount on portfolio 2022-10-05 00:33:05 -05:00
Sinclair Chen
ed6ea011c2 copy: "Dating docs" -> "Dating" 2022-10-04 18:33:26 -07:00
James Grugett
83d33792aa Loan repaid => Loan payment 2022-10-04 20:10:26 -05:00
sipec
583c5b225e Auto-remove unused imports 2022-10-05 00:49:36 +00:00
Sinclair Chen
9f256aa7a8 refactor: require label on buy/confirm buttons 2022-10-04 17:48:24 -07:00
mantikoros
7a271fce29 fix button 2022-10-04 19:35:53 -05:00
James Grugett
d8ef363f06 Add profit amount to sell dialog. 2022-10-04 19:35:29 -05:00
James Grugett
8043fa515a Show loan repaid in sell dialog 2022-10-04 19:10:45 -05:00
mantikoros
f551e6c469 market close styling 2022-10-04 19:07:06 -05:00
Sinclair Chen
3f0b665753 Add Mriya to charities list 2022-10-04 16:57:05 -07:00
Sinclair Chen
40b07329bd Make follow & unfollow buttons same size 2022-10-04 16:42:17 -07:00
Ian Philips
7b9aeea0bd Ignore similar bettor's followed user's markets 2022-10-04 17:12:07 -06:00
Ian Philips
935ff7b97a
Personalized interesting markets emails [WIP] (#1001)
* Test new personalized emails in prod - logs only

* fix import
2022-10-04 16:47:06 -06:00
ingawei
c115b5cca7
Inga/multiple colors (#994)
* making FR bars smoller
2022-10-04 17:36:03 -05:00
Marshall Polaris
d6bb27f97c
Fix graph area to invert at 0 (#998) 2022-10-04 14:13:53 -07:00
Marshall Polaris
bbce3e873e
Tooltip follows marker on charts with marker (#997)
* Renaming of some tooltip stuff

* Tooltip follows marker on single value history charts
2022-10-04 14:02:44 -07:00
mantikoros
26f5e506b7 sell button warning 2022-10-04 15:53:00 -05:00
ingawei
5adaa7253f
made slice skinnier (#996) 2022-10-04 15:41:48 -05:00
Marshall Polaris
a55d85d4b6
Implement basic graph tooltip slice marker thingy (#995) 2022-10-04 12:55:51 -07:00
Ian Philips
f085df96e3 Allow wider group pills on mobile 2022-10-04 09:23:07 -06:00
Ian Philips
1d2af2900b Remove spaces in hashtags & line clamp metadata 2022-10-04 09:14:27 -06:00
Ian Philips
a48cec63fc Use line-clamp in sharing card 2022-10-04 09:06:23 -06:00
Ian Philips
e6374c4994 Fix title, send out the remaining emails today 2022-10-04 08:55:44 -06:00
Ian Philips
6ac467764d Fix unsubscribe all update 2022-10-04 08:38:20 -06:00
Ian Philips
79af4b2be0 Compare to the const ya sillyhead 2022-10-04 08:11:04 -06:00
Ian Philips
094bcaea17 Convert confirmation daisy buttons to tailwind 2022-10-04 08:03:21 -06:00
Ian Philips
c6e5e04e65 Convert confirmation daisy buttons to tailwind 2022-10-04 08:02:20 -06:00
Pico2x
ee4d3947b8 moar contracts in mentions 2022-10-04 14:38:07 +01:00
Marshall Polaris
45b281fac5 Kill a console log 2022-10-04 01:34:17 -07:00
Marshall Polaris
31c6cb7739
Rewrite portfolio graphs with new machinery (#986)
* Fix chart `onMouseOver` propagation

* Make generic charts support money on y-axis

* Fix somewhat ridiculous `formatMoney` to work with negative amounts

* Make margins on charts configurable

* Implement color as function of point on SingleValueHistoryChart

* Rewrite portfolio history graphs with new graph machinery

* Toast Nivo
2022-10-04 01:18:22 -07:00
mantikoros
23ca3ff56a order book label 2022-10-03 23:56:39 -05:00
Sinclair Chen
c3ffac34a1
Remove tag parsing (#956)
* Remove #tag parsing

* Remove #tag linkifying

* lint
2022-10-03 18:28:21 -07:00
Sinclair Chen
375a4e089f
Hide spoiler content in emails / notifs (#957) 2022-10-03 18:25:44 -07:00
James Grugett
efd83eaad4 Home: Don't show duplicate contracts across groups 2022-10-03 18:28:41 -05:00
James Grugett
8d70dc4800 Increase trending group count again. (It's uglier, but it's way more useful!) 2022-10-03 18:22:18 -05:00
Ian Philips
a1dcf8d168 Add best sort to FR contracts 2022-10-03 16:38:12 -06:00
James Grugett
84aaeece9f Filter out unlisted from trending, new, and daily trending on home page 2022-10-03 17:33:51 -05:00
James Grugett
27d765a4a1 Add most popular sort 2022-10-03 17:28:31 -05:00
James Grugett
5214f27be3 Fix daily prob showing global daily prob 2022-10-03 17:23:22 -05:00
mantikoros
d0d223f7ad Auto-prettification 2022-10-03 22:21:56 +00:00
mantikoros
0c9226de41 EditableCloseDate: add pencil icon 2022-10-03 17:20:53 -05:00
ingawei
ce48016f80
added mana cannot be traded for real money disclaimer in welcome modal (#990) 2022-10-03 16:38:10 -05:00
Marshall Polaris
1515d8cab2 Fix lint on Austin's script 2022-10-03 14:30:34 -07:00
Marshall Polaris
28cad9caf8
Bump heroicons yarn.lock version to 1.0.6 (#992) 2022-10-03 14:22:31 -07:00
Marshall Polaris
9a950dc080
Mark scripts as modules to avoid global name collision (#991) 2022-10-03 14:21:21 -07:00
Ian Philips
42cc07e4a6 Hide market title in notifs if grouped 2022-10-03 14:59:27 -06:00
Ian Philips
a5490c903f Reduce streak and max to 5, 25 respectively 2022-10-03 14:52:21 -06:00
Ian Philips
71975f307c Show resolve loading indicator 2022-10-03 14:33:00 -06:00
Ian Philips
ae4136348d Unique bettors email default on 2022-10-03 14:12:55 -06:00
Sinclair Chen
67de983aac Fix links in spoilers 2022-10-03 11:55:10 -07:00
Sinclair Chen
59b128dbe7
Redo add funds modal without daisy and actually use it (#963) 2022-10-03 10:15:58 -07:00
Ian Philips
074a1fdde2 Hide sprig on non-prod envs 2022-10-03 10:59:18 -06:00
Austin Chen
7c34805eeb Upload script to bulk-resolve markets from API 2022-10-03 12:52:48 -04:00
Pico2x
77a5f8b9dd Revert the merge revert (double revert) 2022-10-03 17:31:07 +01:00
Ian Philips
5ae9049295 Show resolution panel above recommented markets 2022-10-03 10:11:24 -06:00
Ian Philips
d5d1284306 Properly handle null id 2022-10-03 09:57:27 -06:00
Ian Philips
adb8bc476f Show whether market is unlisted 2022-10-03 09:36:49 -06:00
Ian Philips
f92f098f82 Allo creators to unlist markets 2022-10-03 09:26:39 -06:00
Ian Philips
370edec890 Remove unsubscribe options for market closure 2022-10-03 08:30:21 -06:00
FRC
f5a3abf0bc
Add spinner (#987) 2022-10-03 15:27:15 +01:00
Ian Philips
27e6534d94 Persist preferred comment sort order by contract 2022-10-03 08:15:27 -06:00
Ian Philips
1caf75d3b5 Do not refund comment bounties 2022-10-03 07:49:26 -06:00
Ian Philips
051c2905e1
Allow user to opt out of all unnecessary notifications (#974)
* Allow user to opt out of all unnecessary notifications

* Unsubscribe from all response ux

* Only send one response
2022-10-03 07:41:39 -06:00
Pico2x
1f7b9174b3 Update index.tsx 2022-10-03 13:45:38 +01:00
FRC
06571a3657
Flag incorrectly resolved markets, warn about unreliable creators (#945)
* Flag incorrectly resolved markets, warn about unreliable creators

* Address James' review nits

* Added a loading state and some copy-changes

* Fix missing refactor

* Fix vercel error

* Fix merging issues
2022-10-03 10:49:19 +01:00
Pico2x
3fb43c16c4 Revert "Merge branch 'main' of https://github.com/manifoldmarkets/manifold"
This reverts commit 603201a00f, reversing
changes made to b517817ee3.
2022-10-03 10:02:38 +01:00
Pico2x
603201a00f Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-10-03 08:47:23 +01:00
Pico2x
b517817ee3 Fix indentation iphone 2022-10-03 08:47:21 +01:00
James Grugett
80693620f0 Make alt contract card listen for updates 2022-10-02 22:46:04 -05:00
mantikoros
f1ae54355d cowp: pointer cursor 2022-10-02 20:44:24 -05:00
Marshall Polaris
503038d2a2 Fix a dumb bug on pseudo-numeric charts 2022-10-02 16:59:49 -07:00
Marshall Polaris
bf8dca25b2
Rewrite stats graphs using new machinery (#985)
* Make curve configurable on generic charts

* Extract SizedContainer helper component

* Use new charts for stats page

* Move analytics charts component

* Fix up start date logic for graphs excluding data
2022-10-02 16:56:29 -07:00
James Grugett
a82f447965 Fix free response comment threading 2022-10-02 18:20:37 -05:00
FRC
1f8c72b4c9
Overview page on groups (#961)
* Frontpage on groups

wip

* Fix James's nits
2022-10-03 00:02:31 +01:00
James Grugett
40c51c3d59 Add emojis to /labs 2022-10-02 17:14:11 -05:00
James Grugett
86ceea831b Add stats to /labs 2022-10-02 17:10:18 -05:00
mantikoros
efb9ef7602 add padding to embeds 2022-10-02 17:04:28 -05:00
James Grugett
8c1131ebab Tweak home search bar spacing on mobile 2022-10-02 17:04:04 -05:00
mantikoros
2c223160ed comment button styling 2022-10-02 16:58:04 -05:00
mantikoros
11bd658c68 hide comment sort, trade tab if no items 2022-10-02 16:58:04 -05:00
James Grugett
39638a3888 Update mtg link 2022-10-02 15:27:29 -05:00
James Grugett
234820ecd4 Add /labs SEO 2022-10-02 15:24:02 -05:00
James Grugett
4d996c2476 Margin tweak 2022-10-02 15:23:16 -05:00
mantikoros
9ecf10496c Auto-remove unused imports 2022-10-02 20:17:27 +00:00
mantikoros
42b27fcedd update midterms dashboard 2022-10-02 15:15:37 -05:00
jahooma
7bf59bcdd0 Auto-prettification 2022-10-02 20:15:07 +00:00
James Grugett
043b18da0e Add referral link to your user page 2022-10-02 15:13:03 -05:00
mantikoros
64951e691e update midterms dashboard 2022-10-02 15:11:40 -05:00
James Grugett
9a90cc3835 Move manalinks into labs 2022-10-02 15:03:29 -05:00
James Grugett
10e361bcac Load daily movers at top level as well 2022-10-02 14:59:02 -05:00
James Grugett
a7f6cb7cfa Fix labs layout 2022-10-02 14:51:28 -05:00
James Grugett
359a768e14 Move challenges into /labs 2022-10-02 14:49:08 -05:00
James Grugett
42aea03415 Add search bar to home 2022-10-02 14:41:44 -05:00
James Grugett
0fb263efa4 Revert "Test loading user from localstorage on first render"
This reverts commit 701d0a06cd.
2022-10-02 14:16:54 -05:00
James Grugett
747977556b Add /labs to More menu 2022-10-02 14:13:19 -05:00
James Grugett
37e8cfbbed Tweak padding 2022-10-02 14:12:33 -05:00
James Grugett
701d0a06cd Test loading user from localstorage on first render 2022-10-02 14:08:05 -05:00
Sinclair Chen
0ffd6c129a
Make small embeds into cards (#976)
* Fix embed style (adjust input, strikethrough)

* Turn small embeds into contract cards

* Use media query instead of conditional render

* Open embed card clicks in new tab
2022-10-02 11:55:47 -07:00
James Grugett
758dbfe398 Add labs cards 2022-10-02 13:51:42 -05:00
James Grugett
33dfce3e16 Remove dating docs from More menu 2022-10-02 13:43:14 -05:00
James Grugett
af66d94c84 Manifold labs 2022-10-02 13:42:44 -05:00
mantikoros
290a34bc64 useTracking 2022-10-02 13:39:29 -05:00
mantikoros
4c2f9011d0 track embed hostname 2022-10-02 13:39:19 -05:00
mantikoros
57b592b5aa show toast after comment tips 2022-10-02 12:55:58 -05:00
mantikoros
fd31b7eaca set comment sort default to newest 2022-10-02 12:50:49 -05:00
Sinclair Chen
1d645e5ff8 trim copy on sort & bounty tooltips 2022-10-02 08:52:53 -07:00
mantikoros
0b0b84a6ad show tips on own comments again 2022-10-01 16:22:19 -05:00
mantikoros
2baae33a77 show market tip total 2022-10-01 16:16:34 -05:00
mantikoros
fac87f8e0c tips: display total 2022-10-01 16:10:17 -05:00
mantikoros
670c6faea8 tip button: remove border color 2022-10-01 16:00:39 -05:00
mantikoros
09e4864b32 consistent tip amount (M$10) 2022-10-01 15:57:47 -05:00
mantikoros
a445d9b7fa make tip button green 2022-10-01 15:54:14 -05:00
mantikoros
cb613705e9
Consistent tips (#984)
* consistent tip button

* hide tips for self

* prettier
2022-10-01 15:51:08 -05:00
mantikoros
aeeb47bdbe don't block on tipping 2022-10-01 15:06:09 -05:00
mantikoros
0844e5620a create: remove visilbity section 2022-10-01 14:30:31 -05:00
mantikoros
2d6fe308b8 better group sort 2022-10-01 14:14:03 -05:00
James Grugett
759685258a Turn off autofocus for amount input. (Fixes FR answer bug; IMO better UX) 2022-10-01 13:48:13 -05:00
James Grugett
b53e4acea6 API: Cache markets for 15 seconds at least 2022-10-01 13:37:56 -05:00
Marshall Polaris
2f1221f094
Size-aware chart tooltip positioning (#980) 2022-10-01 00:10:17 -07:00
mantikoros
2f3ae5192e embed: disable clicking contract details 2022-09-30 20:30:45 -05:00
James Grugett
b0b1d72ba6 Cleaner home page loading! 2022-09-30 20:07:50 -05:00
Marshall Polaris
dc0b6dc6a6
Don't render stuff whenever window size changes (#978) 2022-09-30 18:01:48 -07:00
Marshall Polaris
89e26d077e
Clean up chart sizing code (#977)
* Clean up chart sizing code

* Do all the chart sizing work in same batch
2022-09-30 16:57:48 -07:00
Marshall Polaris
38b7c898f6
More refactoring to make chart tooltips more flexible (#975) 2022-09-30 16:16:04 -07:00
Austin Chen
1fc2f15dae Try extending /stats to 180 days 2022-09-30 18:46:54 -04:00
Sinclair Chen
3d146dd57d decrease trending group count 2022-09-30 14:52:51 -07:00
ingawei
a219680701
Inga/scroll to top (#965)
- adding scroll to top button for markets, removing predict button at the bottom of comments
2022-09-30 15:16:27 -05:00
James Grugett
1e2df99054 Change format money to round up if within epsilon 2022-09-30 15:05:49 -05:00
Sinclair Chen
37beb584ef fix comment bounty overflow style 2022-09-30 12:54:48 -07:00
IanPhilips
9815e7301f Auto-prettification 2022-09-30 19:48:04 +00:00
Ian Philips
ac97e62f2e Add portfolio updates to notification settings 2022-09-30 13:45:57 -06:00
mantikoros
17d1b8575c comment bounty styling 2022-09-30 14:45:23 -05:00
Ian Philips
a25acbe1db Parse ian's email prefs on dev 2022-09-30 13:36:34 -06:00
Phil
b2f81c1149
Twitch minor fix (#973)
* Made Twitch copy link buttons links so right-click -> copy URL works.

* Added Twitch OBS screenshot to public folder.
2022-09-30 20:01:51 +01:00
James Grugett
9d81e3b6d1 Fix import 2022-09-30 13:22:10 -05:00
James Grugett
ab883ea777 Order home group sections by daily score. 2022-09-30 12:00:16 -05:00
Ian Philips
3677de58c3 Add tooltip and badge on contract for bounties 2022-09-30 10:00:55 -06:00
Ian Philips
31de3636fd Fix comment tab title 2022-09-30 09:34:58 -06:00
Ian Philips
a90b765670
Bounty comments (#944)
* Adding, awarding, and sorting by bounties

* Add notification for bounty award as tip

* Fix merge

* Wording

* Allow adding in batches of m250

* import

* imports

* Style tabs

* Refund unused bounties

* Show curreantly available, reset open to 0

* Refactor

* Rerun check prs

* reset yarn.lock

* Revert "reset yarn.lock"

This reverts commit 4606984276.

* undo yarn.lock changes

* Track comment bounties
2022-09-30 09:27:42 -06:00
Ian Philips
55f854115c Remove green circle from resolution prob input 2022-09-30 08:48:33 -06:00
Ian Philips
138f34fc66 Add close now button to contract edit time 2022-09-30 08:40:46 -06:00
Ian Philips
c16e5189f7 Don't send portfolio email to user less than 5 days old 2022-09-30 07:53:47 -06:00
Marshall Polaris
1bc1debbe8 Fix default sizes on charts to make more sense 2022-09-30 00:05:36 -07:00
Marshall Polaris
608ee7b865
Chart visual style adjustment (#971)
* Adjust area fill opacity on line charts

* Light gray border on tooltips
2022-09-30 00:03:31 -07:00
mantikoros
95c47aba1a midterms: add CO, additional markets 2022-09-30 01:30:45 -05:00
James Grugett
f892c92e26 Save portfolio sort and filter to local storage! 2022-09-30 01:11:04 -05:00
Marshall Polaris
7e91133229
Change styles on contract tooltips to be more like portfolio graph (#966) 2022-09-29 22:45:51 -07:00
Marshall Polaris
523689b525
Keep tooltip within bounds of chart (well, for non-FR charts) (#970) 2022-09-29 22:45:31 -07:00
ingawei
b83e5db563
getting rid of daisy buttons (#969)
* getting rid of daisy buttons so bet button does not turn black on mobile
2022-09-30 00:41:22 -05:00
James Grugett
13b3613460 Show number of limit orders 2022-09-29 23:57:45 -05:00
Marshall Polaris
715bae57e0
Fix date memoization in charts (#972)
* Memoize on numbers, not dates

* Use numbers instead of dates to calculate visible range
2022-09-29 21:35:20 -07:00
Marshall Polaris
5b5a919ed7
Expose onMouseOver chart event to hook into from outside (#967) 2022-09-29 20:18:33 -07:00
Ian Philips
2625ab1549 Portfolio email ux 2022-09-29 18:13:33 -06:00
ingawei
262183e0e6
Inga/quick toggle fix (#964)
getting rid of unused component
2022-09-29 18:53:36 -05:00
Sinclair Chen
b7df1a7043
Add ||spoilers|| (#942)
* Add ||spoilers||
* Add spoiler button to format menu
2022-09-29 14:28:04 -07:00
Marshall Polaris
8929b2e6ba
Improve typing for chart tooltip stuff (#962) 2022-09-29 12:51:38 -07:00
mantikoros
9fc1e855ff portfolio graph: put profit first 2022-09-29 13:53:43 -05:00
Pico2x
1755fb15d4 SEO for posts 2022-09-29 19:38:36 +01:00
Olivia Appleton
1e6b72059e
Expose multiple choice answer probabilities (#939)
* Expose multiple choice answer probabilities

* Run prettier

* Update api.md

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-29 14:17:52 -04:00
Olivia Appleton
2d1fd07834
Add documentation for newer market types (#934) 2022-09-29 13:27:07 -04:00
Ian Philips
ec1a9fab77 Show change in M$ 2022-09-29 13:06:12 -04:00
James Grugett
2cc08ba9e7 Daily movers cleanup 2022-09-29 11:42:17 -05:00
Ian Philips
35aa6c0429 Test sample of users' portfolios 2022-09-29 12:32:47 -04:00
Ian Philips
cd7ddae133 Add profit of bets made within last week 2022-09-29 12:30:58 -04:00
Sinclair Chen
46fab105d9 Fix tipper icon progression 2022-09-29 07:26:19 -07:00
Sinclair Chen
4cc985634a Put slider z-index under bottom menu 2022-09-29 07:04:11 -07:00
Marshall Polaris
15cd8b1f94
Fix a couple small chart bugs (#960)
* Fix time clamping causing little visual glitch

* Fix tick formatting glitch
2022-09-28 23:27:42 -07:00
Marshall Polaris
8862425120
Clean up chart tooltip handling (#959) 2022-09-28 21:43:04 -07:00
Marshall Polaris
be010da9f5
Refactor chart tooltip stuff, add bet avatar to tooltips (#958)
* Use objects instead of tuples for chart data

* Carry bet data down into charts

* Refactor to invert control of chart tooltip display

* Jazz up the chart tooltips with avatars

* Tidying
2022-09-28 21:14:34 -07:00
Marshall Polaris
7f7e7acd61
Make binary and pseudonumeric charts re-render less on contract diff (#955) 2022-09-28 18:03:30 -07:00
Sinclair Chen
1f2c7271b7 put edit profile z-index below side menu 2022-09-28 14:30:00 -07:00
Marshall Polaris
83de206e9e
Simply don't print zero (#954) 2022-09-28 14:20:28 -07:00
James Grugett
d55cedb36c Load comments via static props 2022-09-28 13:11:26 -04:00
James Grugett
eb762d9b9e Make loading more sequential for updateMetrics to prevent firebase error. 2022-09-28 12:28:40 -04:00
James Grugett
dba938032f Listen for updates on daily mover contract 2022-09-28 12:28:40 -04:00
ingawei
7c8e977d60
order book things (#953)
Adding order book to limit orders in mobile modal. This is pretty ugly and just a quick fix because people are complaining.
2022-09-28 09:04:47 -05:00
ingawei
e0e6838711 Auto-remove unused imports 2022-09-28 13:46:41 +00:00
ingawei
513cf7b290 added order book 2022-09-28 06:45:32 -07:00
Marshall Polaris
89c3ea559c
Clamp time range in history chart scales (#952) 2022-09-28 01:18:11 -07:00
Marshall Polaris
9238b20242
Modularize d3 imports (#951) 2022-09-28 01:00:39 -07:00
Marshall Polaris
925a9e850f
Hack up brush rendering to fix possible Chrome bug (#950) 2022-09-28 00:58:51 -07:00
Marshall Polaris
8f88af4e2a
Fix an edge case with chart mouseover tooltips (#949) 2022-09-28 00:56:43 -07:00
Marshall Polaris
5b54e7d468
Limit max width of FR legend tooltip labels (#948) 2022-09-27 22:25:37 -07:00
Pico2x
f52127237e COWP for cows 2022-09-28 01:21:38 -04:00
Pico2x
95f2604479 Cowp SEO friendly 2022-09-28 01:04:38 -04:00
Pico2x
a5b943965c Create cowp.tsx 2022-09-28 00:59:24 -04:00
Marshall Polaris
c16adb9ec9
Fix potential clock sync issues with graph updating (#947) 2022-09-27 21:18:22 -07:00
Marshall Polaris
e0d9b4d335
Rewrite contract graphs (#935)
* Fiddle around with everything, WIP FR charts

* Implement numeric chart

* Reorganize everything into neat little files

* Add `AreaWithTopStroke` helper

* Tidying, don't gratuitously use d3.format

* Remove duplicate code

* Better tooltip bisection

* `NumericPoint` -> `DistributionPoint`

* Add numeric market tooltip

* Make numeric chart bucket points less wrong

* Clean up numeric bucket computation

* Clean up a bunch of tooltip stuff, add FR legend tooltips

* Fix a dumb bug

* Implement basic time selection

* Fix fishy Date.now inconsistency bugs

* Might as well show all the FR outcomes now

* Make tooltips accurate on curveStepAfter charts

* Make log scale PN charts work properly

* Adjust x-axis tick count

* Display tooltip on charts only for mouse

* Fix up deps

* Tighter chart tooltips

* Adjustments to chart time range management

* Better date formatting

* Continue tweaking time selection handling to be perfect

* Make FR charts taller by default
2022-09-27 20:24:42 -07:00
James Grugett
9dc0d1696e Fix bug 2022-09-27 19:36:32 -04:00
James Grugett
a7abdbb1db Add to dating group 2022-09-27 19:10:35 -04:00
James Grugett
13dad9a10c Date doc: Remove photo as first-class feature 2022-09-27 19:03:14 -04:00
Austin Chen
14c008234a Script: Add liquidity to all markets in a group 2022-09-27 18:55:30 -04:00
Austin Chen
b87e29d7c0 Rename script 2022-09-27 18:55:30 -04:00
James Grugett
3ed29877ce Add dating docs to menu bar 2022-09-27 18:55:08 -04:00
mantikoros
80d4bffc95
US Elections map (#943)
* usa map

* state election map

* senate midterms

* iframe

* fix

* /midterms

* listen for updates
2022-09-27 17:50:43 -05:00
James Grugett
b21daa1248
Date docs on Manifold (#941)
* Date docs

* Create date doc

* Create and show a date market as well

* Move url to date-docs

* Date doc individual page

* Add share button

* Edit date docs

* Layout

* Add comments for create-post

* Add comments and back nav

* Fix urls

* Tweaks
2022-09-27 17:30:07 -05:00
Austin Chen
419c7ab636 Navigate to ?tab=portfolio 2022-09-27 17:16:48 -04:00
Barak Gila
e2047210b7
add to queue rather than invoking sprig object directly, as it's still being setup (#940) 2022-09-27 15:13:11 -04:00
mantikoros
5e34b5a911 greyscale bet button if outcome is undefined 2022-09-27 13:15:13 -04:00
mantikoros
723d9dbece
Better bet summary (#936)
* show position, expected value, profit instead of "invested"

* move bet summary outside trades on market page

* refactor

* pass in userbets

* hide only if no bets; show invested on desktop

* various
2022-09-27 12:09:54 -05:00
Barak Gila
7ba19c274b
basic sprig integration with possible page URL events (#932)
* basic sprig integration with possible page URL events

* iteration 0

* iteration 1

* run prettier; attempt to remove expect error

* readd expect error messages

* typescript comment fixes

* add identify

* remove package-lock.json

* extract to separate file

* fix linting

* fix lint

* fix lint

* fix missing config
2022-09-27 12:02:03 -05:00
ingawei
a12ed78813
Getting rid of console log, fixing multiple choice markets (#938) 2022-09-26 23:48:00 -05:00
mantikoros
aa93ec060d user page: put markets first 2022-09-27 00:46:30 -04:00
ingawei
fd90bc353b multiple choice betting fix 2022-09-26 21:40:56 -07:00
ingawei
e17a59ae23
Inga/mobilebetting (#911)
* mobile binary betting
2022-09-26 19:28:54 -05:00
ingawei
2fe9fe593d
Inga/profile (#937)
- Changed edit profile button
- got rid of banner
- merged stats and trades tab on profile
- made multicolored profit graph
2022-09-26 18:01:13 -05:00
Ian Philips
d612192109 Send market close notifs for each close time 2022-09-26 18:13:15 -04:00
SirSaltyy
13cffcdaf1 Merge branch 'main' of https://github.com/manifoldmarkets/manifold 2022-09-26 18:12:28 -04:00
SirSaltyy
1b9811ce28 Update twitch page copy 2022-09-26 18:12:24 -04:00
Ian Philips
3ed3b6fb42 Set email sent flag if skipped over 2022-09-26 18:05:50 -04:00
Ian Philips
f7bf42d2e0 Rename & correct spelling 2022-09-26 17:54:48 -04:00
Ian Philips
df316fc4da
Portfolio update emails (#928)
* Stats computing correctly

* Styles propagating - testing in prod now

* Formatting html

* Reset portfolio flag on mondays at 12am

* Add profit, styling

* More styling, less reports

* Cleanup

* Comments

* comment

* Try to send higher signal emails

* Send emails to proper email address
2022-09-26 17:49:06 -04:00
James Grugett
2ef025a151 Only set daily score on contracts that are at least day old 2022-09-26 17:43:27 -04:00
James Grugett
90eaf83775 Redirect from '/home' to '/' if not logged in 2022-09-26 17:04:08 -04:00
Ian Philips
94ffac287e Payout resolution notifications styling 2022-09-26 15:57:38 -04:00
Ian Philips
a10e4c115e Fix dpm MULTI resolution payouts bug 2022-09-26 15:57:21 -04:00
Ian Philips
cc3b44891b Add user to market followers in create answer 2022-09-26 15:56:47 -04:00
Ian Philips
d9292f7a95 Switch order of my groups and all tabs 2022-09-26 11:30:41 -04:00
Ian Philips
bf92c4fb06 Fix 500 on non-existant group page 2022-09-26 11:28:54 -04:00
James Grugett
68120ec2b2 Revert "Clean up and fix stuff on answers panel (#914)"
This reverts commit 721448f408.
2022-09-25 23:29:13 -04:00
Marshall Polaris
be2c60d3f3
Fix some rendering issues on contract page (#933)
* Memoize calculating sale amount on your bets list

* Don't re-render more than necessary with `useIsMobile` hook

* Use `useIsMobile` hook in `AmountInput`
2022-09-25 16:43:53 -07:00
Austin Chen
c1c3a360fd Add CART contest to /tournaments 2022-09-25 13:08:34 -04:00
Austin Chen
ae4d49d960 Generate markets for the Criticism and Red Teaming contest 2022-09-25 11:29:59 -04:00
James Grugett
21c7130d3b Filter out markets with undefined probChanges in dev 2022-09-23 18:58:05 -04:00
Marshall Polaris
d990bc2f07
Remove images config from next.config.js (#931) 2022-09-23 14:55:27 -07:00
Marshall Polaris
e2a8df6c3a
Nivo 0.74.0 -> 0.80.0 (#929) 2022-09-23 14:55:17 -07:00
Marshall Polaris
96dc060a0a
Move react-masonry-css dependency to web package.json (#930) 2022-09-23 14:55:06 -07:00
Austin Chen
d04304bdac Fix blank page on nav to groups 2022-09-23 17:04:32 -04:00
Austin Chen
2891a47d8c Support navigating to /about pages 2022-09-23 16:49:14 -04:00
James Grugett
490734db00 If no user, show loading on home 2022-09-23 16:43:23 -04:00
James Grugett
77ddc456a2 Add new home section to top. 2022-09-23 16:39:17 -04:00
James Grugett
1a5dcdedcc Delay prefetch by 1000ms. Don't prefetch portfolio history. 2022-09-23 16:30:44 -04:00
James Grugett
0ab82a7bd4 Delete some unused code 2022-09-23 15:40:48 -04:00
James Grugett
deb8397ee9 Add Daily Trending section (daily-score for you.) Remove recently updated 2022-09-23 15:33:50 -04:00
James Grugett
57190e7876 Daily trending sort option 2022-09-23 15:33:50 -04:00
FRC
5a10132e2b
Add a "Posts" tab to groups (#926)
* Add a "Posts" sidebar item to groups

* Fix James's nits

* Show "Add Post" button only to users
2022-09-23 15:11:50 -04:00
Sinclair Chen
ebcecd4fe9 remove unused files 2022-09-23 15:01:48 -04:00
Austin Chen
61a9224a7d Move Civid Dashboard and Research.Bet to Alumni 2022-09-23 12:10:48 -04:00
Austin Chen
47c97c36db Add Alignment Markets to Awesome Manifold 2022-09-23 12:01:33 -04:00
Ian Philips
5483955590 Remove contractId from required JSON for /close 2022-09-23 10:21:11 -04:00
Ian Philips
91f89ccb3d Add docs fo /close, allow to pass closeTime 2022-09-23 10:14:41 -04:00
Ian Philips
08202c3ede Add close market endpoint 2022-09-23 10:02:40 -04:00
jahooma
70bc5b2c4a Auto-prettification 2022-09-22 21:58:40 +00:00
James Grugett
c6d034545a
Home: Prob change cards. Sort by daily score. (#925)
* Add dailyScore: product of unique bettors (3 days) and probChanges.day

* Increase memory and duration of scoreContracts

* Home: Smaller prob change card for groups. Use dailyScore for sort order (algolia)

* Add back hover
2022-09-22 16:57:48 -05:00
Sinclair Chen
eaaa46294a fix empty comment send button style 2022-09-22 17:07:51 -04:00
Sinclair Chen
2240db9baa fix profile tab styling 2022-09-22 16:24:57 -04:00
Marshall Polaris
a1c3d0a2dd
Fix up comment permalink stuff (#915)
* Eliminate needless state/effects to highlight comments

* Scroll to comment on render if highlighted
2022-09-22 12:58:40 -07:00
Marshall Polaris
7704de6904
Next.js 12.2.5 -> 12.3.1 (#922) 2022-09-22 12:46:48 -07:00
Marshall Polaris
721448f408
Clean up and fix stuff on answers panel (#914) 2022-09-22 12:40:55 -07:00
Marshall Polaris
6ee8d90bdb
Eliminate redundant showReply/replyTo state (#917) 2022-09-22 12:40:44 -07:00
Marshall Polaris
6fe0a22a48
Improve contract leaderboard computation (#918)
* Fix and clean up top comment stuff

* Make leaderboard code generic on entry type

* No need to look up users on contract leaderboard
2022-09-22 12:40:27 -07:00
mantikoros
b9fffcfa30 sort: add back 24h volume, remove most traded 2022-09-22 14:20:44 -04:00
mantikoros
0c0e7b5582 Auto-prettification 2022-09-22 18:02:17 +00:00
mantikoros
06db5515f6 add qr code to share dialog 2022-09-22 14:01:37 -04:00
FRC
a5e293c010
Bring back tabs in groups (#923) 2022-09-22 12:12:53 -04:00
Sinclair Chen
4412d0195c
Add tooltips to market header icons (#924) 2022-09-22 11:53:55 -04:00
mantikoros
c15285aa64 pare down sorts; only show high/low prob on groups 2022-09-22 00:32:20 -04:00
Austin Chen
9ff2b62740 Remove console log 2022-09-21 23:10:25 -04:00
Sinclair Chen
e9ab234d61 copy: manifold dollars -> mana 2022-09-21 17:49:32 -07:00
ingawei
7988fdde60
simplify binary graphs (#921) 2022-09-21 18:49:20 -05:00
Pico2x
b875ac563d Revert "Bring back tabs in groups (#919)"
This reverts commit b4a59cfb21.
2022-09-21 19:14:05 -04:00
FRC
b4a59cfb21
Bring back tabs in groups (#919) 2022-09-21 18:27:49 -04:00
Austin Chen
d922900bda Increase tip size to M$10 2022-09-21 18:25:56 -04:00
ingawei
24766740c5
cleaning up search bar for mobile (#916)
* cleaning up search bar for mobile
2022-09-21 16:48:32 -05:00
Austin Chen
73fad2e34b Remove F2P Tournament 2022-09-21 15:31:45 -04:00
Marshall Polaris
a10605e74c
More work on contract page and tabs (#912)
* Consolidate comment thread component code

* Move `visibleBets` work down into bets tab

* Remove unnecessary cruft from contract page props

* Don't load all comments in contract page static props anymore

* Tidy up props a bit

* Memoize bets tab

* Memoize recommended contracts widget
2022-09-21 00:02:10 -07:00
Marshall Polaris
c7f29af2ee
Clean up some stuff in AnswersPanel (#902)
* Tidy up messy markup on FR answers panel

* Clean up obsolete feed-related answer stuff

* Slight fixup per James feedback
2022-09-20 22:07:40 -07:00
James Grugett
ea1579975c Increase memory of update functions 2022-09-20 23:56:14 -05:00
Marshall Polaris
6e2aa622ab
Refactor, improve efficiency of contract tabs stuff (#909)
* Move comments and tips fetching down into comments tab rendering

* Consolidate `contract-activity.tsx` into `contract-tabs.tsx`

* Move LP fetching into bets tab
2022-09-20 21:02:17 -07:00
mantikoros
54778ec1b1
Fix twitch onboarding (#910)
* don't show welcome dialog for twitch users

* handle sign up race conditions with more hooks

* content organization and copy tweaks

* lint

* fix import
2022-09-20 19:23:18 -05:00
Marshall Polaris
8870f0d356
Don't always require tips to render comments (#898) 2022-09-20 15:58:47 -07:00
Marshall Polaris
be4def49a2
Kill counts of comments and trades on contract page (#900) 2022-09-20 15:53:35 -07:00
James Grugett
589bf9651d Track viewing full daily movers 2022-09-20 17:40:31 -05:00
Marshall Polaris
60c79141aa
Move comment-bet association code into comment creation trigger (#899)
* Move comment-bet association code into comment creation trigger

* Add index for new comments query
2022-09-20 15:25:58 -07:00
Marshall Polaris
faaf502114
Remove old check in free answer comment rendering (#906) 2022-09-20 14:11:26 -07:00
Marshall Polaris
30ce80d0c9
Extract signup UI from contract tabs component (#901) 2022-09-20 14:04:07 -07:00
Marshall Polaris
8145b128ad
Move recommended contracts to own widget (#896) 2022-09-20 14:03:52 -07:00
Marshall Polaris
a2d9e8e3d2
Cleanup free answer comment stuff (#897)
* Remove unused most-recent-bet-time stuff

* Remove strange reply box hiding behavior

* Tidying markup
2022-09-20 14:03:33 -07:00
mantikoros
106dc232b8 send market guide onboarding email after 96 hrs 2022-09-20 16:03:17 -05:00
mantikoros
379e736e51
hide liquidity panel (#904) 2022-09-20 15:57:27 -05:00
mantikoros
8920241c39 space out onboarding emails 2022-09-20 15:56:28 -05:00
mantikoros
ac952f1164 Revert "Don't send creator guide email & interesting markets on create user"
This reverts commit a4399aaee9.
2022-09-20 15:49:46 -05:00
Ian Philips
6d7fbd69c7 Lint 2022-09-20 12:17:37 -04:00
Ian Philips
a4399aaee9 Don't send creator guide email & interesting markets on create user 2022-09-20 12:15:23 -04:00
Ian Philips
6c3338f5d7 Remove unused unsubscribe attributes 2022-09-20 09:59:48 -04:00
Ian Philips
272ba921a0 Add memory to weekly email functions 2022-09-20 09:45:14 -04:00
Pico2x
fdd7dcc0ab Rm group about/short description 2022-09-20 14:42:41 +01:00
Ian Philips
5ab86c8362 Check new weekly email notification preferences 2022-09-20 09:36:44 -04:00
Ian Philips
c6a60a6678 Streak & uniques bonus in transaction 2022-09-20 08:42:09 -04:00
James Grugett
62f20694bf Exclude resolved from daily movers 2022-09-20 00:49:25 -05:00
James Grugett
c338dce3ce Add daily activation rate. Remove top tenth actions. Cleanup 2022-09-20 00:10:05 -05:00
marsteralex
44deaf7b0a
WIP: add artist category (#866)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

* added difficulty rating

* updated padding

* add dfc support for commanders

* add artists

* use latest non-digital if possible

* change vsCode settings for python

* update with latest non-digital printing

* update artist list

* update algo to select k samples

* cleanup code

* equally weight artists

* weight everything equally

* updated for all artists

* update artists

* add allowlist

* update artists to min 50 art

* allow promo to be replaced

* update jsons

* update with min 100 arts

* update code to be smaller jsons

* updated to 18 artists per game

* update ui

* update importing artists

* update to 21

* move num artists to top of js file

* update artistList to not include artist sigs

* update to 50 artists

* update for ub

* update artist list

* update ub defaults

* update jsons

* allow non-english cards to be replaced

* update allowlist

* update jsons

* add watermark

* update jsons

* update jsons

* make jsons slightly smaller

* add checkmarks and x's

* remove python

* add no answer and checkbox and x

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-19 18:10:14 -07:00
James Grugett
4dc3eada1f Add more stats. Fix timezone. Group retention and new user retention 2022-09-19 18:41:24 -05:00
James Grugett
d0973de2b4 Use percent chart, save unrounded stats 2022-09-19 17:45:52 -05:00
James Grugett
b4244ea75d Change stats date to be by Pacific time zone 2022-09-19 17:10:12 -05:00
mantikoros
a2b01e28c9 daily movers: filter out numeric markets 2022-09-19 16:29:21 -05:00
James Grugett
935c550733 Revalidate static props on market resolve 2022-09-19 16:26:34 -05:00
jahooma
c101337c38 Auto-remove unused imports 2022-09-19 21:25:50 +00:00
James Grugett
f3ff6d99c8 Remove colored background for daily movers to make it not feel like your own profit / loss 2022-09-19 16:24:36 -05:00
James Grugett
de8e4df04c Revalidate static props for new comments 2022-09-19 16:07:27 -05:00
James Grugett
1a82ce193d Add node-fetch, add secret to function config 2022-09-19 15:25:16 -05:00
James Grugett
fb27fac524 Revalidate getStaticProps after each bet 2022-09-19 14:55:37 -05:00
James Grugett
6f5d69ec9c Increase memory of updateMetrics function 2022-09-19 14:34:48 -05:00
mantikoros
55a68d4fec email wording 2022-09-19 14:22:04 -05:00
mantikoros
24cf42284f replace "predictor" => "trader" 2022-09-19 14:03:52 -05:00
FRC
6aa45a2d12
Move group navbar to top (#895) 2022-09-19 17:29:17 +01:00
Ian Philips
5d65bb5bb1 Add message about unique bonuses withdrawn on n/a 2022-09-19 07:31:04 -06:00
Marshall Polaris
bfe00595e7
Make comments with bet outcome but no answer outcome appear (#894) 2022-09-19 00:53:10 -07:00
James Grugett
b93af31d2f Add D1 and W1 (new users) to stats 2022-09-19 01:28:18 -05:00
Marshall Polaris
a9e5020904
Render free response comment threads more simply without bets (#893) 2022-09-18 19:16:48 -07:00
Marshall Polaris
58dcbaaf6e
Precalculate and store current positions for users who make comments (#878) 2022-09-18 15:57:50 -07:00
mantikoros
e37b805b49 disable liquidity bonus (for now) 2022-09-18 17:49:29 -05:00
James Grugett
8ebf829169 Change groups default sort to Trending 2022-09-18 17:16:13 -05:00
James Grugett
56b4889b94 Add user.homeSections to firestore rules 2022-09-18 17:00:35 -05:00
James Grugett
17453e5618 Improve hook that was spamming in dev 2022-09-18 16:57:20 -05:00
James Grugett
ae6437442b Fix unsaved changes warning erronously appearing 2022-09-18 16:36:45 -05:00
James Grugett
373cfc5d10 Format firestore /group rules 2022-09-18 16:23:09 -05:00
mantikoros
540915eb65 homepage: fix betting streaks error 2022-09-18 16:05:22 -05:00
James Grugett
f111d6e24f Fix console errors from svg non-camelcase attributes 2022-09-18 15:55:39 -05:00
James Grugett
676bcc159d Fix missing key 2022-09-18 15:53:04 -05:00
James Grugett
1da4373335 Creating a group from create market adds it immediately 2022-09-18 14:24:29 -05:00
James Grugett
c9e782faa7 Simplify create group dialog 2022-09-18 13:49:28 -05:00
Marshall Polaris
39119a3419
Twitch bot deployment work (#892)
* Point at production Twitch bot endpoint

* Move Twitch endpoints into env config
2022-09-18 01:13:10 -07:00
James Grugett
65166f2fcb Fix import 2022-09-18 01:10:34 -05:00
James Grugett
eb021f30f5 Fix loans (user without a portfolio throws error) 2022-09-18 01:05:55 -05:00
James Grugett
4aea3b96d7 Save initial home sections for new users 2022-09-17 23:58:18 -05:00
James Grugett
987274ad2d Fix bug part 2 2022-09-17 20:01:00 -05:00
James Grugett
2166169608 Fix bug 2022-09-17 19:56:20 -05:00
James Grugett
d8e9e7812a Don't show Daily movers if there are none. Threshold is 1% 2022-09-17 19:47:04 -05:00
James Grugett
3bddda37d2 Add plus to trending group button 2022-09-17 19:25:19 -05:00
jahooma
42f66b11f4 Auto-prettification 2022-09-18 00:20:50 +00:00
James Grugett
436646cc47 Use algolia to fetch daily movers so it's faster. 2022-09-17 19:18:48 -05:00
James Grugett
a14e7d3947 Move Algolia bits to own file in web/lib/service 2022-09-17 19:18:48 -05:00
mantikoros
47cc313aef add back leaderboards link 2022-09-17 19:15:26 -05:00
mantikoros
44f9a1faa2 fix labels 2022-09-17 19:12:44 -05:00
mantikoros
f71791bdd5 fix labels 2022-09-17 19:10:34 -05:00
James Grugett
350ab35856 Tweak padding 2022-09-17 18:52:14 -05:00
mantikoros
37cff04e39 share dialog styling 2022-09-17 18:49:24 -05:00
James Grugett
e7ed893b78 Round prob in Daily movers 2022-09-17 18:45:40 -05:00
mantikoros
8f30ef38d9 fix imports 2022-09-17 18:40:45 -05:00
James Grugett
1fbadf8181 Improve Customize UI 2022-09-17 18:30:29 -05:00
mantikoros
438c12da57 refactor sidebar; add to mobile navbar 2022-09-17 18:26:02 -05:00
James Grugett
191ec9535c Show more rows on daily movers all 2022-09-17 18:00:24 -05:00
James Grugett
b74fd57912 Show absolute prob in daily movers as well 2022-09-17 17:58:08 -05:00
James Grugett
a54f060ccb New for you => New 2022-09-17 15:15:37 -05:00
mantikoros
fdde57e334 'predictor' => 'trader' 2022-09-17 15:10:16 -05:00
mantikoros
fde90be5a2 fix resolve prob notification text 2022-09-17 15:01:45 -05:00
mantikoros
d2471e2a02 group selector dialog: loading indicator, tracking 2022-09-17 14:59:02 -05:00
James Grugett
f35799c129 Only autofocus search if no query params set 2022-09-17 14:54:55 -05:00
mantikoros
6a21067440 update /about redirect 2022-09-17 14:46:59 -05:00
mantikoros
340b21c53e halve referral bonus 2022-09-17 14:38:52 -05:00
mantikoros
fc5807ebbe halve MAX_QUESTION_LENGTH 2022-09-17 14:38:52 -05:00
Austin Chen
e0806cf0e0 Fix links to group /about and /leaderboards 2022-09-16 20:36:52 -07:00
James Grugett
94c448ee8b Most predictions => Most traded 2022-09-16 17:43:27 -05:00
Sinclair Chen
3e9f046a29 Always focus search bar on search page 2022-09-16 15:37:52 -07:00
James Grugett
9340d827d9 Replace new with recently updated 2022-09-16 17:29:46 -05:00
James Grugett
612066d96c Tweak margin 2022-09-16 17:26:00 -05:00
James Grugett
015e86afcb Add search to bottom nav bar. Add back Home title to home page. 2022-09-16 17:24:31 -05:00
James Grugett
7c710ba598 Rename file nav-bar to bottom-nav-bar 2022-09-16 17:24:31 -05:00
Sinclair Chen
5b8fc12163
Make add group button same height as group names (#890) 2022-09-16 15:21:07 -07:00
James Grugett
ab3ed3fbf1 Save last sort to local storage 2022-09-16 17:09:12 -05:00
James Grugett
22d5c74818 Add search nav items 2022-09-16 16:46:24 -05:00
James Grugett
1321139e7f Prefetch daily movers 2022-09-16 16:35:04 -05:00
mantikoros
70ef9e1836 group sidebar, navbar tweaks 2022-09-16 16:30:06 -05:00
James Grugett
e4cfd92bb2 Track home actions 2022-09-16 16:21:13 -05:00
James Grugett
25ee793208
🏠 New home (#889)
* Factor out section header

* Remove daily balance change

* Remove dead code

* Layout, add streak

* Fix visibility observer to work on server

* Tweak

* Search perserved by url

* Add pill query param

* Add search page

* Extract component for ProbChangeRow

* Explore groups page

* Add search row

* Add trending groups section

* Add unfollow option for group

* Experimental home: accommodate old saved sections.

* Tweaks to search layout

* Rearrange layout

* Daily movers page

* Add streak grayed out indicator

* Use firebase query instead of algolia search for groups

* Replace trending group card with pills

* Hide streak if you turned off that notification

* Listen for group updates

* Better UI for adding / removing groups

* Toast feedback for join/leave group. Customize button moved to bottom.

* Remove Home title

* Refactor arrange home

* Add new for you section

* Add prefetch

* Move home out of experimental!

* Remove unused import

* Show non-public markets from group
2022-09-16 16:12:24 -05:00
mantikoros
f7164ddd7d
group selector dialog (#888)
* group selector dialog

* cache groups to prevent ui jumping

* welcome dialog display logic

* show fewer groups, more filtering
2022-09-16 14:58:36 -05:00
Sinclair Chen
dd2b09830e update Clearer Thinking end date to Sep 30 2022-09-16 12:13:45 -07:00
Phil
52ecd79736
Twitch prerelease (#887)
* Bot linking button functional.

* Implemented initial prototype of new Twitch signup page.

* Removed old Twitch signup page.

* Moved new Twitch page to correct URL.

* Twitch account linking functional.

* Fixed charity link.

* Changed to point to live bot server.

* Slightly improve spacing and alignment on Twitch page

* Tidy up, handle some errors when talking to bot

* Seriously do the thing where Twitch link is hidden by default

* Fixed secondary Get Started button. Copy overlay and dock link now functional.

* Add/remove bot from channel working.

* Removed legacy Twitch controls from user profile.

* Links provided by dock/overlay buttons are now correct.

* Minor profile cleanup post merge.

* Fixed unnecessary relinking Twitch account when logging in on Twitch page.

* Added confirmation popup to refresh API key. Refreshing API key now requires a user to relink their Twitch account.

* Removed legacy twitch-panel.tsx

Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-09-16 08:43:49 -07:00
IanPhilips
c316d49957 Auto-prettification 2022-09-16 15:30:09 +00:00
Ian Philips
68f2277def Just put 'you' on mobile 2022-09-16 09:28:39 -06:00
Ian Philips
a2d912bb5a Add more info to limit order notif 2022-09-16 09:02:58 -06:00
Ian Philips
c183315d52 Don't notify of updated close time when resolving market 2022-09-16 08:15:16 -06:00
Ian Philips
6a5873f8d4 Try more restrictive detault notification settings 2022-09-16 07:43:27 -06:00
FRC
456aed467c
Move tabs to sidebar (#873)
* Move tabs to sidebar

* Address all feedback

Fix icon names
Extract navbar component into a separate function
Rm arrow and indentation
Move group name under logo
Fix visual sidebar stretchy thing
Fix visual bug

* Extra nits
2022-09-16 14:32:15 +01:00
ingawei
256fd89fd2
market close fix oopsies (#886)
* market close fix
2022-09-16 02:38:09 -05:00
Phil
833ec518b4
Twitch prerelease (#882)
* Bot linking button functional.

* Implemented initial prototype of new Twitch signup page.

* Removed old Twitch signup page.

* Moved new Twitch page to correct URL.

* Twitch account linking functional.

* Fixed charity link.

* Changed to point to live bot server.

* Slightly improve spacing and alignment on Twitch page

* Tidy up, handle some errors when talking to bot

* Seriously do the thing where Twitch link is hidden by default

Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-09-16 00:22:13 -07:00
Austin Chen
1321b95eb1
%mentions for embedding contract card, take 2 (#884)
* Revert "Revert "Use %mention to embed a contract card in rich text editor (#869)""

This reverts commit e0634cea6d.

* Overwrite name to prevent breakages

* Fix '%' mentioning if you escape out

* Cleanup: merge render functions
2022-09-15 23:37:17 -07:00
Austin Chen
ca4a2bc7db Remove console log 2022-09-15 23:00:58 -07:00
mantikoros
430ad1acb0 "unique bettors"; "Unknown" => "0" 2022-09-15 23:18:27 -05:00
ingawei
1ce989f3d6
Inga/bettingfix embedfix (#885)
* Revert "Revert "Inga/bettingfix (#879)""
This reverts commit 176acf959f.
* added embed fix
2022-09-15 19:41:25 -05:00
mantikoros
5a1cc4c19d getCpmmInvested: fix NaN issue 2022-09-15 18:32:38 -05:00
James Grugett
e0634cea6d Revert "Use %mention to embed a contract card in rich text editor (#869)"
This reverts commit 140628692f.
2022-09-15 18:19:22 -05:00
Sinclair Chen
ebbb8905e2
Add clearer thinking Regrant to tournaments (#883) 2022-09-15 16:05:56 -07:00
Austin Chen
140628692f
Use %mention to embed a contract card in rich text editor (#869)
* Bring up a list of contracts with @

* Fix hot reload for RichContent

* Render contracts as half-size cards

* Use % as the prompt; allow for spaces

* WIP: When there's no matching question, create a new contract

* Revert "WIP: When there's no matching question, create a new contract"

This reverts commit efae1bf715.

* Rename to contract-mention

* WIP: Try to merge in @ and % side by side

* Add a different pluginKey

* Track the prosemirror-state dep
2022-09-15 15:12:26 -07:00
Ian Philips
e9fcf5a352 Space 2022-09-15 16:12:05 -06:00
Ian Philips
3362b2f953 Capitalize 2022-09-15 15:51:39 -06:00
Ian Philips
61c672ce4c Show negative payouts 2022-09-15 15:50:26 -06:00
Ian Philips
7628713c4b Enrich contract resolved notification 2022-09-15 15:25:19 -06:00
Marshall Polaris
b903183fff
Paginate contract bets tab (#881)
* Apply pagination to bets list on contract

* Make contract trades tab number actually match number of entries
2022-09-15 13:47:07 -07:00
Austin Chen
1476f669d3 Fix capitalization 2022-09-15 13:45:51 -07:00
Ian Philips
8c6a40bab7 Enrich limit order notification 2022-09-15 13:39:46 -06:00
Sinclair Chen
69c2570ff9 fix copy to make clear referrals aren't limited 2022-09-15 12:29:57 -07:00
Ian Philips
b3e6dce31e Capitalize 2022-09-15 09:57:14 -06:00
Ian Philips
be91d5d5e0 Avatars don't link during contract selection 2022-09-15 09:51:52 -06:00
Ian Philips
e9f136a653 Single source of truth for predict 2022-09-15 09:12:56 -06:00
Ian Philips
4c10c8499b Take back unique bettor bonuses on N/A 2022-09-15 09:12:44 -06:00
Pico2x
718218c717 Update bet-inline.tsx 2022-09-15 15:51:14 +01:00
Pico2x
772eeb5c93 Update [contractSlug].tsx 2022-09-15 15:45:49 +01:00
Ian Philips
ada9fac343 Add logs to on-create-bet 2022-09-15 08:07:42 -06:00
Ian Philips
733d206517 Add txn types 2022-09-15 07:50:35 -06:00
Ian Philips
4a5c6a42f6 Store bonus txn data in data field 2022-09-15 07:45:11 -06:00
Ian Philips
e5428ce525 Watch market modal copy 2022-09-15 07:14:59 -06:00
Pico2x
176acf959f Revert "Inga/bettingfix (#879)"
This reverts commit 8aaaf5e9e0.
2022-09-15 13:55:57 +01:00
ingawei
8aaaf5e9e0
Inga/bettingfix (#879)
* making betting action panels much more minimal, particularly for mobile
* added tiny follow button
2022-09-15 01:46:58 -05:00
ingawei
ccf02bdba8
Inga/admin rules resolve (#880)
* Giving admin permission to resolve all markets that have closed after 7 days.
2022-09-14 22:28:40 -05:00
Ian Philips
9aa56dd193 Only show prev opened notif setting section 2022-09-14 17:25:17 -06:00
Ian Philips
3efd968058 Allow one-click unsubscribe, slight refactor 2022-09-14 17:17:32 -06:00
Sinclair Chen
68b0539fc1 Enable search exclusion and exact searches
like `-musk` to remove Elon results or `"eth"` for Ethereum results
2022-09-14 15:06:11 -07:00
Sinclair Chen
7aaacf4d50 Center tweets 2022-09-14 13:19:12 -07:00
Ian Philips
050bd14e46 Update script 2022-09-14 10:29:48 -06:00
Ian Philips
7ba2eab65e Rename user notification preferences 2022-09-14 10:26:08 -06:00
Ian Philips
edbae16c8e Betting streak reset indicator 2022-09-14 08:56:05 -06:00
Ian Philips
d6b0a1edc0 Betting streak reset to 7am UTC and store streak data on notif 2022-09-14 07:27:20 -06:00
mantikoros
a2d61a1daa
Twitch integration (#815)
* twitch account linking; profile page twitch panel; twitch landing page

* fix import

* twitch logo

* save twitch credentials cloud function

* use user id instead of bot id, add manifold api endpoint

* properly add function to index

* Added support for new redirect Twitch auth.

* Added clean error handling in case of Twitch link fail.

* remove simulator

* Removed legacy non-redirect Twitch auth code. Added "add bot to channel" button in user profile and relevant data to user type.

* Removed unnecessary imports.

* Fixed line endings.

* Allow users to modify private user twitchInfo firestore object

* Local dev on savetwitchcredentials function

Co-authored-by: Phil <phil.bladen@gmail.com>
Co-authored-by: Marshall Polaris <marshall@pol.rs>
2022-09-14 01:52:31 -07:00
Marshall Polaris
7144e57c93
Denormalize user display fields onto bets (#853)
* Denormalize user display fields onto bets

* Make bet denormalization script fast enough to run it on prod

* Make `placeBet`/`sellShares` immediately post denormalized info
2022-09-14 01:33:59 -07:00
Marshall Polaris
1ebb505752
Fix liquidity feed display to look right (#877) 2022-09-14 01:13:53 -07:00
mantikoros
273b815e54 hide house liquidity on feed 2022-09-14 00:51:43 -05:00
mantikoros
e7d8cfe7e0
House liquidity (#876)
* add house liquidity for unique bettors

* hide notifications from house liquidity

* up bonus liquidity to  M$20
2022-09-14 00:26:47 -05:00
mantikoros
be851b8382 fix typo 2022-09-13 21:23:36 -05:00
mantikoros
58ef43a8ec intro panel: use gradient image 2022-09-13 21:12:01 -05:00
Ian Philips
f6feacfbc9 Fix lint and persistent storage key 2022-09-13 17:18:16 -06:00
Sinclair Chen
74335f2b01
Adjust market modal styles (#875)
* Refactor add market modals into one component
* Adjust style: stickier search, scroll auto
2022-09-13 16:16:07 -07:00
Ian Philips
df3d7b591d Componentize notification line setting, don't use useEffect 2022-09-13 17:00:34 -06:00
James Grugett
c9d323c83f
Small updates to experimental/home (#874)
* Factor out section header

* Remove daily balance change

* Remove dead code

* Layout, add streak

* Fix visibility observer to work on server

* Tweak

* Formatting
2022-09-13 17:47:29 -05:00
Ian Philips
34bad35cb8 Betting=>predicting 2022-09-13 16:19:52 -06:00
Ian Philips
c423326270 Send users emails when they hit 1 and 6 unique bettors 2022-09-13 16:12:53 -06:00
Ian Philips
4398fa9bda Add new market from followed user email notification 2022-09-13 09:54:51 -06:00
Ian Philips
2c922cbae6 Send no-bet resolution emails to those without bets 2022-09-13 08:16:23 -06:00
Ian Philips
55b895146b Find multiple choice resolution texts as well 2022-09-13 07:54:37 -06:00
Ian Philips
8b1776fe3b Remove contracts number badge from groups tab 2022-09-13 07:53:01 -06:00
Ian Philips
de8c27c970 Filter None answer earlier 2022-09-13 07:48:41 -06:00
James Grugett
483838c1b2 Revert "Make parse.richTextToString more efficient (#848)"
This reverts commit cb143117e5.
2022-09-12 19:06:37 -05:00
Marshall Polaris
cb143117e5
Make parse.richTextToString more efficient (#848) 2022-09-12 16:11:03 -07:00
Sinclair Chen
22d2248951
Add floating menu (bold, italic, link) (#867)
* Add floating menu (bold, italic, link)
* Sanitize and href-ify user input
2022-09-12 16:10:32 -07:00
Ian Philips
2351403674 Replies to answers are comments 2022-09-12 17:04:06 -06:00
Ian Philips
018eb8fbfc Send notif to all users in reply chain as reply 2022-09-12 17:01:59 -06:00
James Grugett
f49cb9b399 Only show 'Show more' for free response answers if there are more answers to show 2022-09-12 17:40:19 -05:00
mantikoros
d66a81bc6b Auto-prettification 2022-09-12 22:35:32 +00:00
mantikoros
8e41b39936 landing page: use next image for logo 2022-09-12 17:34:13 -05:00
mantikoros
0e5b1a7742 market intro panel 2022-09-12 17:30:51 -05:00
mantikoros
3d3caa7a42 remove comment bet area 2022-09-12 16:50:38 -05:00
Pico2x
a3da8a7c3c Make update-metrics actually write cached group leaderboards 2022-09-12 22:01:37 +01:00
Pico2x
2a96ee98f4 Fix type error in update metrics pt.3 2022-09-12 21:49:15 +01:00
Ian Philips
5c6fe08bdb Website => Web 2022-09-12 14:48:42 -06:00
Ian Philips
747d5d7c7c In app => website 2022-09-12 14:48:16 -06:00
Ian Philips
3a814a5b5d Detect just settings tab w/o section 2022-09-12 14:41:30 -06:00
Ian Philips
e35c0b3b52 Only notify followers of new public markets 2022-09-12 14:36:54 -06:00
Ian Philips
0af1ff112b Allow users to see 0% FR answers via show more button 2022-09-12 14:30:15 -06:00
Pico2x
4456a771fd fix type error in update-metrics pt.2 2022-09-12 21:25:45 +01:00
Ian Philips
86422f90ea Set all overflow notifs to seen 2022-09-12 14:17:39 -06:00
Pico2x
7d9908dbd0 Fix type error in update-metrics 2022-09-12 20:58:12 +01:00
FRC
ff81b859d1
"Fix "500 internal error" in large groups (#872)
* Fix "500 internal error" in large groups (#856)

This reverts commit 28f0c6b1f8.

* Ship without touching prod and with some logs.
2022-09-12 20:54:11 +01:00
Ian Philips
3cb36a36ec Separate email and browser ids list 2022-09-12 11:00:24 -06:00
James Grugett
4f19220778 Experimental home: accommodate old saved sections. 2022-09-12 11:56:20 -05:00
Ian Philips
5c6328ffc2
[WIP] Fully customizable notifications (#860)
* Notifications Settings page working

* Update import

* Linked notification settings to notification rules

* Add more subscribe types

* It's alive... It's alive, it's moving, it's alive, it's alive, it's alive, it's alive, IT'S ALIVE'

* UI Tweaks

* Clean up comments

* Direct & highlight sections for notif mgmt from emails

* Comment cleanup

* Comment cleanup, lint

* More comment cleanup

* Update email templates to predict

* Move private user out of getDestinationsForUser

* Fix resolution messages

* Remove magic

* Extract switch to switch-setting

* Change tab in url

* Show 0 as invested or payout

* All emails use unsubscribeUrl
2022-09-12 10:34:56 -06:00
FRC
28f0c6b1f8
Revert "Fix "500 internal error" in large groups (#856)" (#871)
This reverts commit a6ed8c9228.
2022-09-12 17:26:46 +01:00
FRC
a6ed8c9228
Fix "500 internal error" in large groups (#856)
* Members to memberIds

* Moved to update-metrics
2022-09-12 16:44:24 +01:00
James Grugett
c1287a4a25
Small updates to experimental home (#870)
* Line clamp question in prob change table

* Tweaks

* Expand option for daily movers

* Snap scrolling for carousel

* Add arrows to section headers

* Remove carousel from experimental/home

* React querify fetching your groups

* Edit home is its own page

* Add daily profit and balance

* Merge branch 'main' into new-home

* Make experimental search by your followed groups/creators

* Just submit, allow xs on pills

* Weigh in

* Use next/future/image component to optimize avatar images

* Inga/challenge icon (#857)

* changed challenge icon to custom icon
* fixed tip button alignment

* weighing in and trading "weigh in" for "trade"

* Delete closing soon, mark new as New for you, trending is site-wide

* Delete your trades. Factor out section item

* Don't allow hiding of home sections

* Convert daily movers into a section

* Tweaks for loading daily movers

* Prob change table shows variable number of rows

* Fix double negative

Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-09-12 00:39:04 -05:00
Sinclair Chen
f8d346a404 Clean up charity styles
- center cards on mobile
- make notes more professional
2022-09-11 11:42:05 -07:00
mantikoros
93033b5b24 Revert "Yes and no buttons on contract page (#868)"
This reverts commit b39e0f304f.
2022-09-10 21:57:50 -05:00
mantikoros
18815caed4 Revert "Put sale value above quick bet button"
This reverts commit 9ee7173305.
2022-09-10 21:57:35 -05:00
Ian Philips
9ee7173305 Put sale value above quick bet button 2022-09-10 17:48:35 -06:00
Ian Philips
b39e0f304f
Yes and no buttons on contract page (#868)
* Yes and no buttons on contract page

* Cheating by adding 0.05 to max shares but gives better quickbet UX
2022-09-10 17:07:23 -06:00
mantikoros
e17234ecce typo 2022-09-10 17:43:52 -05:00
Austin Chen
33bcc1a65e Clean up /tournaments styling 2022-09-10 12:00:01 -07:00
Austin Chen
e61591622e Feature a few other semi-tournaments 2022-09-10 11:50:03 -07:00
jahooma
11ba65ec4a Auto-remove unused imports 2022-09-09 22:43:33 +00:00
FRC
26f83ac4f6
Adds investmentValue to group leaderboard calculation (#855)
* Adds investmentValue to group leaderboard calculation

* Initial investment is no longer counted, only the profit

* Simplify scoring calculation

* Remove console.log

* Group bets by user first

Co-authored-by: James Grugett <jahooma@gmail.com>
2022-09-09 17:42:51 -05:00
akrolsmir
cca870ced5 Auto-remove unused imports 2022-09-09 21:27:16 +00:00
Austin Chen
fdf123b875 Remove console.logs from common code
This makes it easier to debug in local; we shouldn't be checking in console.log into the codebase, as a general rule
2022-09-09 14:26:23 -07:00
Austin Chen
a737ae9f46 Link to tournament /about pages 2022-09-09 14:14:22 -07:00
mantikoros
43660387fa modal positioning 2022-09-09 16:08:42 -05:00
mantikoros
7729bdd2dc bet panel: higher threshold for warning; no autofocus on mobile 2022-09-09 15:58:26 -05:00
marsteralex
1ae0f0e273
add dfc support to commanders (#865)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

* added difficulty rating

* updated padding

* add dfc support for commanders

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-09 13:54:54 -07:00
Austin Chen
18466afc78 Fix API URLs from non "manifold.markets" domains 2022-09-09 11:43:23 -07:00
Austin Chen
4c801f76b4
Submit comments on ctrl/cmd-enter (#830)
* Submit comments on ctrl/cmd-enter

* Remove unused import

* Tweak padding on /tournaments

* Always submit on ctrl+enter

Since we took out group chats, this should be fine for all comments
2022-09-09 11:09:31 -07:00
Austin Chen
6a69f44f07 Tweak padding on /tournaments 2022-09-09 10:16:25 -07:00
marsteralex
aa5876fe0d
added difficulty rating (#864)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

* added difficulty rating

* updated padding

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-09 09:51:20 -07:00
Ian Philips
e639cb654e Add group endpoint note in api docs 2022-09-09 07:22:31 -06:00
Austin Chen
1408908959 List the Manifold F2P tournament 2022-09-09 01:46:25 -07:00
mantikoros
cd1d8ecd8a WarningConfirmationButton for bets 2022-09-09 01:02:30 -05:00
mantikoros
0dbb42aa69 Auto-prettification 2022-09-09 05:03:05 +00:00
mantikoros
2ebb83418c bet panel: disable input focus on mobile 2022-09-09 00:02:14 -05:00
mantikoros
eac56b1f4f slider: smarter step increments; disable clicking on track 2022-09-09 00:02:14 -05:00
James Grugett
987ebccdfd Contract tabs: used passed in bets 2022-09-08 23:45:26 -05:00
James Grugett
cf74a195b2 Redeem shares: pay back a smaller frac of your loan 2022-09-08 22:59:25 -05:00
marsteralex
677b20a7ba
fix brawl commanders for digital (#862)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

* remove generated cards from digital commanders

* fix counterspell setting default

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-08 20:23:31 -07:00
Austin Chen
e3e80a5fd0 Change user info using bulkWriter 2022-09-08 20:21:32 -07:00
marsteralex
8aeb544f7e
add commander category (#861)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

* update cards for categories

update for dominaria united

* added commander category

commander category

* update basic land art

* can use double feature

* removing racist cards upstream

this way we don't have to store the cards in the json

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-08 18:38:48 -07:00
James Grugett
b9ae919fda Add staleTime option for prefetching 2022-09-08 16:59:05 -05:00
mantikoros
f25460a647 smaller font for "Predict" 2022-09-08 15:10:36 -05:00
Sinclair Chen
28f3694e8f
Fix editor rerendering when you load it (#831)
* Don't rerender entire editor for user list

also fixes bug where you are the only mention

* Cache with react query instead of memoize
2022-09-08 13:02:50 -07:00
Pico2x
caa3fc06e6 Minor tailwind/indent fixes to posts 2022-09-08 17:32:42 +01:00
Pico2x
1e645f911a Add Fede to admin pt2 2022-09-08 17:14:40 +01:00
Pico2x
adf2086141 Add Fede to admins 2022-09-08 16:51:58 +01:00
FRC
d9bb7d1926
Edit posts (#859) 2022-09-08 16:23:19 +01:00
Ian Philips
5547b30364 Add david to admins 2022-09-08 09:16:54 -06:00
Ian Philips
3932a3dbd4 I predict this will do better than trade 2022-09-08 07:40:16 -06:00
James Grugett
bff4eff719 Persist user page markets on back (Marshall's machinery) 2022-09-08 01:39:01 -05:00
James Grugett
54c227cf6c
Updates to experimental home (#858)
* Line clamp question in prob change table

* Tweaks

* Expand option for daily movers

* Snap scrolling for carousel

* Add arrows to section headers

* Remove carousel from experimental/home

* React querify fetching your groups

* Edit home is its own page

* Add daily profit and balance

* Merge branch 'main' into new-home

* Make experimental search by your followed groups/creators

* Just submit, allow xs on pills

* Weigh in

* Use next/future/image component to optimize avatar images

* Inga/challenge icon (#857)

* changed challenge icon to custom icon
* fixed tip button alignment

* weighing in and trading "weigh in" for "trade"

Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-09-08 01:36:34 -05:00
mantikoros
edbebb7e67 weighing in and trading "weigh in" for "trade" 2022-09-08 00:16:48 -05:00
ingawei
004671f032
Inga/challenge icon (#857)
* changed challenge icon to custom icon
* fixed tip button alignment
2022-09-07 23:51:52 -05:00
Austin Chen
45a965476e Use next/future/image component to optimize avatar images 2022-09-07 20:59:00 -07:00
Ian Philips
bcee49878b Weigh in 2022-09-07 21:39:21 -06:00
Ian Philips
35de4c485a Just submit, allow xs on pills 2022-09-07 21:39:14 -06:00
James Grugett
4439447a6d Persist group markets and scroll position on back (Marshall's machinery) 2022-09-07 21:33:36 -05:00
mantikoros
e6c6f64077 fix mobile nav for trades tab 2022-09-07 21:16:58 -05:00
FRC
0acdec787d
Adds comments to posts (#844)
* Adds comments to posts

* Uncoupled CommentInput from Contracts

* Fix nits
2022-09-07 23:09:20 +01:00
mantikoros
ce52f21ce9 fix sidebar profile link to your trades 2022-09-07 15:13:17 -05:00
mantikoros
b3343c210a more "bet" => "trade" 2022-09-07 15:04:34 -05:00
mantikoros
b4e0e9ebc0 "A market for every question" 2022-09-07 15:01:02 -05:00
mantikoros
28af2063c3 "bet" => "trade" 2022-09-07 14:45:04 -05:00
FRC
cce14cbe1f
Toggle monthly leaderboards (#790)
* Toggle monthly leaderboards

I didn't get to enabling monthly leaderboards after my work trial was over (I enabled daily/weekly/alltime). The cache has been filled out for a while now, this toggles it on.

* Fix nits
2022-09-07 17:04:30 +01:00
Ian Philips
87060488f5 Convert market to lite market for Phil 2022-09-07 07:13:34 -06:00
James Grugett
ad18987e65 Update Daily movers UI 2022-09-07 01:18:11 -05:00
James Grugett
a40bdc28be Remove some excess spacing on user page 2022-09-06 23:39:50 -05:00
James Grugett
082125bd2f Remove some margin 2022-09-06 23:31:02 -05:00
James Grugett
21870d7edb User page: Move portfolio graph and social stats to new tab 2022-09-06 23:24:58 -05:00
mantikoros
85be84071a track embedded markets separtely 2022-09-06 22:43:28 -05:00
mantikoros
a9627bb2b6 market page: regenerate static props after 5 seconds 2022-09-06 22:12:18 -05:00
Sinclair Chen
537962a7dc Stop links from opening twice 2022-09-06 16:55:33 -07:00
Ian Philips
f7d027ccc9 Create button=>Site link 2022-09-06 16:38:01 -06:00
Ian Philips
8759064ccb new bettors 2022-09-06 16:30:58 -06:00
Ian Philips
c16e7c6cfd Add membership indicators and link to see group 2022-09-06 16:20:43 -06:00
James Grugett
668f30dd55 Free market creation shows cost striked through 2022-09-06 16:55:52 -05:00
Ian Philips
45e54789b7 Groups search shares query, sorted by contract & members 2022-09-06 15:51:36 -06:00
mantikoros
c59de1be2e bet slider: decrease step size 2022-09-06 11:53:09 -05:00
Ian Philips
a038ef91eb Show num contracts in group selector 2022-09-06 09:58:24 -06:00
Ian Philips
74af54f3c0 Remove chance from FR og-images 2022-09-06 09:36:41 -06:00
Ian Philips
7c44abdcd7 Comment out unused script functions 2022-09-06 09:27:50 -06:00
Ian Philips
5af92a7d81 Update groups API 2022-09-06 09:24:26 -06:00
Ian Philips
2ee067c072 Remove member and contract ids from group doc 2022-09-06 08:14:13 -06:00
Ian Philips
39d7f1055b Fix spacing on challenge modal 2022-09-06 07:58:00 -06:00
Ian Philips
a3b18e5bea Add challenge back to share modal 2022-09-06 07:57:52 -06:00
FRC
59f3936dad
Fix bug (#854) 2022-09-06 14:17:21 +01:00
mantikoros
450b140f5f show challenge button on mobile 2022-09-05 18:19:13 -05:00
James Grugett
f21711f3dc Fix type error 2022-09-05 18:13:01 -05:00
James Grugett
cd8bb72f94 Daily movers table in experimental/home 2022-09-05 18:09:03 -05:00
mantikoros
837a4d8949 Revert "Show challenge on desktop, simplify modal"
This reverts commit 8922b370cc.
2022-09-05 18:07:44 -05:00
mantikoros
8952b100ad add answer panel mobile formatting, slider 2022-09-05 17:59:19 -05:00
mantikoros
2d724bf2c8 make slider black 2022-09-05 17:44:21 -05:00
mantikoros
374c25ffb3 Auto-prettification 2022-09-05 22:40:48 +00:00
mantikoros
96cf1a5f7f mobile slider styling 2022-09-05 17:39:59 -05:00
mantikoros
ae40999700 mobile bet slider 2022-09-05 17:11:32 -05:00
mantikoros
30d73d6362 remove parantheses from balance text 2022-09-05 16:59:35 -05:00
mantikoros
97e0a78806 "join group" => "follow" 2022-09-05 16:51:09 -05:00
James Grugett
d812776357 Remove show hot volume param 2022-09-05 16:25:48 -05:00
mantikoros
9a49c0b8fe remove numeric, multiple choice markets from create market page 2022-09-05 13:33:58 -05:00
Austin Chen
70eec63533 Adding in "Highest %" and "Lowest %" sort options
Quick alternative to https://github.com/manifoldmarkets/manifold/pull/850/files courtesy of James.

One downside of this approach is that the % only update every 15 minutes; but maybe users won't notice?
2022-09-05 10:07:33 -07:00
Marshall Polaris
6ef2beed8f
Denormalize betAmount and betOutcome fields on comments (#838)
* Create and use `betAmount` and `betOutcome` fields on comments

* Be robust to ridiculous bet IDs on dev
2022-09-04 14:28:45 -07:00
James Grugett
a15230e7ab Smartest money => Best bet. Don't show amount made for comment. 2022-09-04 14:06:29 -05:00
James Grugett
a21466d877 Add sort for 24 hour change in probability 2022-09-03 16:20:57 -05:00
Marshall Polaris
89b30fc50d Fix tournaments page loading indicator and turn page size back down 2022-09-03 14:07:34 -07:00
James Grugett
9060abde8e Cache prob and prob changes on cpmm contracts 2022-09-03 15:06:42 -05:00
mantikoros
085b9aeb2a remove simulator 2022-09-03 14:58:42 -05:00
Marshall Polaris
c0383bcf26
Make tournament page efficient (#832)
* Make tournament page efficient

* Fix URL to Salem contract

* Use totalMembers instead of deprecated field

* Increase page size to 12

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-09-03 09:55:10 -07:00
Ian Philips
0938368e30 Capitalize yes/no resolution outcomes 2022-09-03 07:29:35 -06:00
Ian Philips
272658e5dc Use most up-to-date user on groups page 2022-09-03 06:52:51 -06:00
Marshall Polaris
861fb7abbd
Use the magic auth prop for groups SSR (#851) 2022-09-03 06:51:55 -06:00
Ian Philips
2d88675f42 Move & more out of the loop 2022-09-03 06:33:33 -06:00
James Grugett
bfa88c3406 Turn off react-query notification subscription because it's buggy 2022-09-02 22:57:23 -05:00
Marshall Polaris
784c081663
Enable source maps in production (#852) 2022-09-02 19:43:22 -07:00
Marshall Polaris
8318621d51
Some changes to make auth better (#846)
* Handle the case where a user is surprisingly not in the DB

* Only set referral info on user after creation

* More reliably cache current user info in local storage

* Don't jam username stuff into user listener hook
2022-09-02 19:39:27 -07:00
Marshall Polaris
e924061c54
Don't re-create visibility observer for no reason (#849)
* Don't re-create visibility observer for no reason

* `IntersectionObserver.unobserve` instead of `.disconnect`
2022-09-02 19:39:07 -07:00
Ian Philips
25a0276bf7 Auth user server-side on groups page 2022-09-02 19:52:38 -06:00
Ian Philips
c74d972caf Pass user and members via props 2022-09-02 19:36:49 -06:00
Ian Philips
57b74a5d09 Use cached values 2022-09-02 18:12:55 -06:00
Ian Philips
9577955d2d Remove null check 2022-09-02 18:08:53 -06:00
Ian Philips
cf508fd8b6
Members and contracts now subcollections of groups (#847)
* Members and contracts now documents

* undo loans change?

* Handle closed group

* Slight refactoring

* Don't allow modification of private groups contracts

* Add back in numMembers

* Update group field names

* Update firestore rules

* Update firestore rules

* Handle updated groups

* update start numbers

* Lint

* Lint
2022-09-02 18:06:48 -06:00
James Grugett
2f53cef36f Move metrics calculation to common 2022-09-02 18:45:42 -05:00
James Grugett
af68fa6c42 Fix typo in email followup 2022-09-02 16:20:04 -05:00
James Grugett
231d3e65c4 Fix incorrect error message for no bets 2022-09-02 16:19:10 -05:00
James Grugett
00de66cd79
Leaderboard calc: update profit even when portfolio didn't change (#845)
* Leaderboard calc: remove didProfitChange optimization that was incorrect

* Put back didPortfolioChange for deciding whether to create new history doc.
2022-09-02 15:59:32 -05:00
mantikoros
b6449ad296 fix bet panel warnings 2022-09-02 15:32:54 -05:00
Marshall Polaris
d1e1937195
Remove custom token generation machinery (#840) 2022-09-02 13:04:00 -07:00
Marshall Polaris
245627a347 Temporarily patch groups loading to make dev deploy work 2022-09-02 13:00:38 -07:00
Marshall Polaris
a429a98a29
Tidy up some dead code and markup in sidebar (#842) 2022-09-02 12:52:27 -07:00
Marshall Polaris
b1bb6fab5b
Disable SSR on /home (#839) 2022-09-02 12:51:41 -07:00
Marshall Polaris
21b9d0efab
Clean up some old pre-Amplitude tracking code (#841) 2022-09-02 12:51:27 -07:00
Marshall Polaris
4c429cd519
Remove some old code related to the old feed (#843) 2022-09-02 12:51:14 -07:00
mantikoros
0cb20d89ed numeric market labels: LOW/HIGH instead of MIN/MAX; eliminate payout <= MIN, etc. 2022-09-02 10:35:41 -05:00
James Grugett
8029ee49a4 Fix loans bug 2022-09-01 23:06:57 -05:00
Marshall Polaris
4406e53121
Make prefetching correctly use context cache (#835) 2022-09-01 19:38:09 -07:00
Marshall Polaris
dca7205a47
Disable group prefetching from contract links (#836)
* Kill dead code

* Stop prefetching groups when viewing contract

* Tidy markup
2022-09-01 19:37:41 -07:00
Sinclair Chen
04e8bb248b Fix Salem Center market url 2022-09-01 18:15:10 -07:00
Austin Chen
51fe44f877 Show the number of open markets on each groups page 2022-09-01 16:12:09 -07:00
Ian Philips
00ba3b0c48
Show avatars of tippers and unique bettors (#837)
* Show avatars of tippers and unique bettors

* Make transparent the avatar bg

* fix import
2022-09-01 16:23:12 -06:00
Marshall Polaris
7508d86c73
Clean up contract overview code (#823)
* Don't call Date.now a million times in answers graph

* Refactor contract overview code so that it's easier to understand
2022-09-01 14:42:50 -07:00
Ian Philips
8d853815d6
Show resolution on og card image (#834)
* Handle resolved markets

* Add in group names as hashtags
2022-09-01 13:55:24 -06:00
FRC
1208694d2d
http to https to avoid blocked requests (#833) 2022-09-01 17:54:46 +01:00
FRC
96be4e8992
Add embedded ContractGrid to Posts (#822)
* Add embedded market grids

* Hacky way to set height

I haven't figured out a way yet to get the height of the actual iframe's content, so I did some bad estimate for now to unblock shipping the feature, while I continue investigating.
2022-09-01 17:47:45 +01:00
Ian Philips
7310cf3d4a fix import 2022-09-01 10:11:08 -06:00
Ian Philips
8922b370cc Show challenge on desktop, simplify modal 2022-09-01 10:02:41 -06:00
Ian Philips
fecf976ab9 Show all group contracts if less than 5 open 2022-09-01 09:11:14 -06:00
Ian Philips
0823414360 Adjust group name padding on mobile 2022-09-01 08:52:49 -06:00
Ian Philips
c6eac97b64 Show group based on most recent creator added group 2022-09-01 08:29:56 -06:00
Ian Philips
6706fe7350 Show user balance on bet panels 2022-09-01 08:12:46 -06:00
Ian Philips
a7c8b8aec4 Hide bet panel when signed out 2022-09-01 07:34:02 -06:00
Ian Philips
5dec6b4a22 Medium includes 10 bettors 2022-09-01 07:23:43 -06:00
Ian Philips
a8d7e91a02 Clean comments 2022-09-01 07:01:49 -06:00
Ian Philips
fec4e19c1d Selectively force long polling for ios only 2022-09-01 07:01:02 -06:00
Marshall Polaris
0568322c82
Dramatically improve server auth stuff (#826) 2022-08-31 22:13:26 -07:00
Marshall Polaris
42548cea2a
Fix prefetching to not populate useless state (#827) 2022-08-31 21:59:58 -07:00
mantikoros
879d6fb2dd bury profile stats in Comments until we find a better place for them 2022-08-31 23:20:20 -05:00
mantikoros
2a17bcb8b2 eslint 2022-08-31 23:00:39 -05:00
Sinclair Chen
7c1e663b26
Editor tweaks (#829)
* Show border around selected embeds

* Make editor tooltips not animate
2022-08-31 20:52:12 -07:00
mantikoros
2c3cd34444 Auto-remove unused imports 2022-09-01 03:34:22 +00:00
mantikoros
e0ebdc644d market close email: remove mention of creator fee 2022-08-31 22:33:37 -05:00
James Grugett
ee76f4188b For you: remove contracts bet on by anyone you follow. 2022-08-31 21:57:11 -05:00
Sinclair Chen
58e671e640 Upload dropped images 2022-08-31 17:18:35 -07:00
Austin Chen
bc1ec414de
Update awesome-manifold.md 2022-08-31 16:29:42 -07:00
Austin Chen
5514eeff2d
Update awesome-manifold.md 2022-08-31 16:18:53 -07:00
Austin Chen
7a9b159909
Update awesome-manifold.md 2022-08-31 15:40:23 -07:00
Ian Philips
74b6df2e44 Unwatch applies to email comment notifs too 2022-08-31 16:18:48 -06:00
Ian Philips
26aba26da5
force long polling (#824) 2022-08-31 15:38:55 -06:00
Austin Chen
7c8b33597a Add "Duplicate Contract" into "..." menu 2022-08-31 14:33:24 -07:00
James Grugett
3660830ec1 Don't server side render Notifications page for improved perf 2022-08-31 15:41:34 -05:00
James Grugett
83696cca21 Fix dayjs fromNow bug (it requires plugin, so use our util instead) 2022-08-31 15:35:47 -05:00
Sinclair Chen
d06b725f52
Let admins add and edit posts to any group (#820)
- show add post UI to admins
- change firebase permissions
2022-08-31 11:29:49 -07:00
Marshall Polaris
149204f6ca Fix my other foolish bug 2022-08-31 11:17:36 -07:00
Ian Philips
5a9d8e3f5d Show how much you've tipped a market 2022-08-31 09:27:37 -06:00
Ian Philips
37d2be9384 Show only relative time if same day on close date 2022-08-31 08:49:35 -06:00
Ian Philips
5df594e46a Make details fit on one line, make group a link 2022-08-31 08:29:35 -06:00
Ian Philips
91e5abe76a Add query to help avoid timeout 2022-08-31 08:03:51 -06:00
Marshall Polaris
27b46f4306 Fix order of comments in threads and under answers 2022-08-31 01:16:57 -07:00
Marshall Polaris
d336383a93 Fix my foolish bug 2022-08-31 01:02:10 -07:00
James Grugett
a3569280a4 Add your bets section to /experimental/home 2022-08-31 00:30:31 -05:00
James Grugett
ccb6fd291e Move components out of /pages into /components 2022-08-30 23:53:12 -05:00
James Grugett
849402ed70 Rearrange home sections. Load more in carousel. 2022-08-30 23:47:09 -05:00
Marshall Polaris
7dddff52b8
Tidying some feed code up (#818)
* Clean up some markup & dead code

* Order comments in Firestore instead of on client

* Order bets in Firestore instead of on client

* Make indexes file up to date with production
2022-08-30 20:28:30 -07:00
mantikoros
40f1c09002 Auto-remove unused imports 2022-08-31 01:56:03 +00:00
mantikoros
ec90b041ee upgrade firebase, nextjs versions 2022-08-30 20:54:32 -05:00
Sinclair Chen
c202c5de68 clarify closed/open group copy 2022-08-30 16:28:49 -07:00
Ian Philips
aad5f6528b
new market view (#819)
* Show old details on lg, don't unfill heart

* Hide tip market if creator

* Small ui tweaks

* Remove contract. calls

* Update high-medium-low

* Remove unused bets prop

* Show uniques

* Remove unused bets prop
2022-08-30 17:13:25 -06:00
James Grugett
3e1e84ee5e Experimental Home: Add links. Single layer carousel for < 6 cards 2022-08-30 17:14:22 -05:00
James Grugett
f83b62cf50 Implement double carousel for /experimental/home 2022-08-30 16:18:42 -05:00
Austin Chen
d658a48b66 Revert "hide quick bet on mobile"
This reverts commit 3d073da97e.
2022-08-30 10:31:35 -07:00
Ian Philips
876abef040 Only send dev weekly trending emails to ian 2022-08-30 10:02:51 -06:00
Ian Philips
74c9406191 Use cached user ids while likes is loading 2022-08-30 09:52:14 -06:00
Ian Philips
a0402830c5
liking markets with tip/heart (#798)
* WIP liking markets with tip

* Refactor Userlink, add MultiUserLink

* Lint

* Lint

* Fix merge

* Fix imports

* wip liked contracts list

* Cache likes and liked by user ids on contract

* Refactor tip amount, reduce to M

* Move back to M

* Change positioning for large screens
2022-08-30 09:38:59 -06:00
FRC
e1f19c52ab
Post in a group about page. (#803)
* Dashboards in Group about page

* Rename group dashboard to 'About Post'

* Fixed James nits
2022-08-30 13:39:10 +01:00
Marshall Polaris
7debc4925e
De-feedify contract tab contents (#808)
* De-feedify contract bets list

* De-feedify contract comments lists

* Clean up a bunch of duplicated work in the comments list stuff

* Remove wrapper markup from comment replies list

* Fix sort order on comments I broke

* Kill now unhelpful `CommentRepliesList` wrapper component

* More random cleanup

* More cleanup and fix some styling I had broken

* Make bet calculations less wrong

* Keep up to date with master

* Make copy link component copy better URL

* Make highlighted comments align properly

* Make user header left align with content on comments

* Fix some free response UI stuff up
2022-08-30 02:41:47 -07:00
Marshall Polaris
1e3a0ca3d9
Upgrade Typescript, ESLint, Prettier (#817)
* Bump Typescript to 4.8.2, eslint, prettier

* Fix some loose typing

* Fix prettier complaint
2022-08-30 01:44:45 -07:00
James Grugett
c7452796f0 Recommend contracts you haven't bet on 2022-08-30 00:22:12 -05:00
Marshall Polaris
1369f3b967
WIP persistence work (#762)
* WIP persistence work

* Fix up close date filter, kill custom scroll restoration

* Use built-in Next.js scroll restoration machinery

* Tweaking stuff

* Implement 'history state' idea

* Clean up and unify persistent state stores

* Respect options for persisting contract search

* Fix typing in common lib

* Clean up console logging
2022-08-29 21:56:11 -07:00
James Grugett
1d948821ca Turn off react query subscription for user bets and portfolio history 2022-08-29 16:47:21 -05:00
Ian Philips
0318f7a12b Add missing parentheses 2022-08-29 13:47:24 -06:00
James Grugett
84432e5ac4 Add creatorId to lite market 2022-08-29 14:25:58 -05:00
FRC
851cffd73e
Dashboards (#791)
* Create backend for Dashboards

* Rm lastupdatetime for now

* Added a create-dashboard and sharable view dashboard page

* Various nit fixes.

* Renamed Dashboards to Posts

* Fix nits
2022-08-29 16:06:17 +01:00
Austin Chen
1d1b09c938 Append question changed text to end of description (instead of start) 2022-08-28 23:23:40 -07:00
Austin Chen
8f338a8d88
Prevent embeds from breaking in Chrome incognito (#814) 2022-08-28 22:40:57 -07:00
James Grugett
7ea6777d6b Add margin bottom to tournament cards to reveal shadow 2022-08-29 00:29:59 -05:00
James Grugett
ecacce0796 Remove console.log. Log onIdTokenChanged error. 2022-08-29 00:26:12 -05:00
James Grugett
71dfcc4dd9 Add tracking for clicking recommended card & tournament card 2022-08-29 00:23:31 -05:00
Austin Chen
6c64c9f1cd Remove hot volume from /tournaments 2022-08-28 22:21:28 -07:00
mantikoros
6facf3b7a7 sidebar ordering 2022-08-29 00:01:04 -05:00
James Grugett
62e72b2091 Loan dialog wording tweak 2022-08-28 23:51:43 -05:00
James Grugett
4dad954820 Change limit order label "at" => "up to" or "down to" 2022-08-28 23:47:11 -05:00
Austin Chen
f0727a65fc Add SF 2022 Ballot to /tournaments 2022-08-28 21:33:13 -07:00
akrolsmir
c7be227865 Auto-remove unused imports 2022-08-29 04:00:14 +00:00
Austin Chen
cf58fc9fd4 Remove Groups from sidebar 2022-08-28 20:59:14 -07:00
James Grugett
996b4795ea
Cache user bets tab with react query!! (#813)
* Convert useUserBets to react query

* Fix duplicate key warnings

* Fix react-query workaround to use refetchOnMount: always'

* Use react query for portfolio history

* Fix useUserBet workaround

* Script to back fill unique bettors in all contracts

* React query for user bet contracts, using uniqueBettorsId!

* Prefetch user bets / portfolio data
2022-08-28 18:03:00 -05:00
mantikoros
7e00f29189 back to "sign up to bet" 2022-08-28 16:55:29 -05:00
mantikoros
1ff453d64c eslint 2022-08-28 16:38:59 -05:00
mantikoros
e4c66e08f5 clean up add markets dialog 2022-08-28 16:26:36 -05:00
mqp
3fd07da1b0 Auto-prettification 2022-08-28 21:15:31 +00:00
mantikoros
eb070f0b07 Auto-remove unused imports 2022-08-28 21:14:44 +00:00
mantikoros
c88621de19 hide group edit dialog when signed out 2022-08-28 16:14:02 -05:00
mantikoros
2e96721a5c "sign in" => "add my answer" 2022-08-28 16:14:02 -05:00
mantikoros
0a5fb4752a CreateQuestionButton: use Button component 2022-08-28 16:14:02 -05:00
mantikoros
cae2154893 sign in button 2022-08-28 16:14:02 -05:00
mantikoros
926929880a "Sign up to bet" => "Place my bet"; "Sign in to comment" => "Add my comment"; rename button to BetSignUpPrompt 2022-08-28 16:14:02 -05:00
James Grugett
9c15d5b96c
React-query-ify notifications (#812)
* Use single react query to subscribe to notifications

* Remove 'preferred' in variable names
2022-08-28 15:20:21 -05:00
mantikoros
3d073da97e hide quick bet on mobile 2022-08-28 14:07:19 -05:00
mantikoros
d63dd12056 admin unlisted toggle 2022-08-28 13:37:34 -05:00
mantikoros
133e7a9c3f change label to admin 2022-08-28 13:37:34 -05:00
mantikoros
98861ccc19 remove typo 2022-08-28 13:37:34 -05:00
Marshall Polaris
1e11491369 Tidy up rendering of info tooltips 2022-08-28 01:43:13 -07:00
James Grugett
7c798a063c Improve edit close date UI 2022-08-28 00:35:24 -05:00
mantikoros
03e07037ea
ban users from posting (#810) 2022-08-28 00:23:25 -05:00
James Grugett
2acc1a8433 Double daily loans rate to 2% 💰💰 2022-08-28 00:11:28 -05:00
James Grugett
9dd23b4a08 Fix weird new crash in updateMetrics: contract.id missing? 2022-08-28 00:11:13 -05:00
James Grugett
e4f46c48f1 Fix recommended markets not updating when navigating 2022-08-27 22:35:46 -05:00
James Grugett
cb08a114ae Better recommended contracts. Include from first group. 2022-08-27 22:26:39 -05:00
James Grugett
e7f369e2b4 Load recommended markets even when navigating from home 2022-08-27 22:26:39 -05:00
mantikoros
f31db2f9ed emails: make banner a link 2022-08-27 22:15:14 -05:00
Marshall Polaris
b21051ced5 Fix up copy link toast styling 2022-08-27 19:15:55 -07:00
Marshall Polaris
ef77c7c9a3
Clean up markup in CopyLinkDateTimeComponent (#809) 2022-08-27 19:05:46 -07:00
James Grugett
36fa9078f5 Fix absolute import within functions 2022-08-27 17:18:39 -05:00
James Grugett
a80d1f194c Don't redeem shares if there's only epsilon shares to redeem 2022-08-27 17:14:41 -05:00
James Grugett
d7793841d1 Fix NaN invested (floating point error) 2022-08-27 17:13:29 -05:00
Marshall Polaris
4b513a894d
Make tooltip rendering more efficient (#807)
* Don't use very slow dayjs formatter on timestamp tooltips

* Kill dead code in feed-bets.tsx

* Clean up tooltip markup
2022-08-27 13:46:35 -07:00
mantikoros
eeed9eef10 market resolution email: link in header image, show only non-negative investment amount 2022-08-27 14:38:09 -05:00
mantikoros
305acbb18f "current value" => "expected value" 2022-08-27 14:17:19 -05:00
Marshall Polaris
5d8f5d41fc
Fix some efficiency problems with ContractProbGraph (#806)
* Memoize bets input to ContractOverview

* Optimize a bunch of nonsense in `ContractProbGraph`
2022-08-27 01:09:17 -07:00
Marshall Polaris
3e976eadac
Make portfolio graph loading more efficient (#805)
* Make portfolio graph on profile not load extra data

* Clean up unused props

* Tidy up markup

* Enable "daily" option again on portfolio history picker
2022-08-27 01:09:01 -07:00
James Grugett
51ceb62871 Fix console error on create page 2022-08-27 01:14:24 -05:00
James Grugett
a9ea335cd1 Fix create page serverside vs clientside render discrepancy console error 2022-08-27 01:07:39 -05:00
James Grugett
a040df2732 Fix console error from non-react-style attributes on trophy icon 2022-08-27 01:02:59 -05:00
James Grugett
2e3c2d4dcb Tweak to add market to group UI 2022-08-27 00:59:00 -05:00
mqp
5ff847fba3 Auto-prettification 2022-08-27 05:01:29 +00:00
jahooma
f641569bcc Auto-remove unused imports 2022-08-27 05:00:37 +00:00
James Grugett
8e4dd407f6 Test with unused import 2022-08-26 23:59:56 -05:00
James Grugett
b88f9a4fc1
Set up github action to remove unused imports 2022-08-26 23:56:38 -05:00
James Grugett
86cf956894 Add eslint plugin to remove unused imports 2022-08-26 23:49:03 -05:00
James Grugett
e4d6bb35b5 Fix floating button to be on top of quick bet arrows. Switch icon. 2022-08-26 23:10:10 -05:00
Marshall Polaris
902d9e140c
Create and use new usePagination hook for paginating loading (#769)
* Create and use new `usePagination` hook for paginating loading

* Fix index for new comment list code
2022-08-26 20:18:08 -07:00
Sinclair Chen
9698895c22 Update fr chart colors 2022-08-26 17:39:46 -07:00
Austin Chen
a2da319e7c Remove unused import 2022-08-26 17:35:59 -07:00
Austin Chen
1dbef921b0 Sort markets on /tournaments by % 2022-08-26 17:13:49 -07:00
Sinclair Chen
59aa76a474
Add Salem Center tournaments (card screenshots) (#804) 2022-08-26 16:23:44 -07:00
James Grugett
99bff6b794 Improve group market selection UI 2022-08-26 18:17:15 -05:00
James Grugett
5735864fd1 Add pencil to edit group on contract page 2022-08-26 17:25:05 -05:00
Sinclair Chen
8903b1ef95
Replace style props with tailwind classes (#800)
* add utility class for `word-break: break-word`

* refactor visuallyHidden style out of Page

* refactor out ref sizing hack in sidebar

* replace style props with tailwind classes
2022-08-26 14:23:06 -07:00
Austin Chen
3255806891 Support Figma embeds 2022-08-26 12:41:39 -07:00
Austin Chen
ba7d0f45db Close Add Market modal on Cancel 2022-08-26 12:41:29 -07:00
Sinclair Chen
490115d890
Add tournaments to sidebar (#802)
* Add tournaments to sidebar

* Remove unused import

* Reposition tournaments tab

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-26 10:45:01 -07:00
Sinclair Chen
803091db06
Add tournament home page (#797)
* Add tournament home page

* Preload markets, follow count

* organize imports

* Fix card width

* Make entire title clickable

* plural /tournament -> /tournaments

* prettier

* Fix /tournaments when groupIds are invalid

* Restyle /tournaments page

* Reintroduce Salem, tweak styles

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-26 10:31:25 -07:00
James Grugett
31db330319 Show "(max)" on streak payout if it is the max payout 2022-08-26 11:38:08 -05:00
James Grugett
b1ccee73fd If there is a group for a market on market page, clicking it goes to group 2022-08-26 01:23:50 -05:00
James Grugett
74ce98913c Make graph start from left side for new markets 2022-08-26 01:08:16 -05:00
Austin Chen
26a2eb2391 Switch to a different color scheme 2022-08-25 22:31:05 -07:00
James Grugett
6f2e2a3bbb Render graph for multiple choice embeds 2022-08-26 00:23:50 -05:00
James Grugett
539bfba70c Decrease starting time window for free response graph 2022-08-26 00:21:06 -05:00
James Grugett
e5777f02d8 Expand notifications by default if <= 3 items 2022-08-26 00:00:44 -05:00
Sinclair Chen
1b029ce8dd
bump tiptap version to fix multi-italic bug (#801) 2022-08-25 20:56:38 -07:00
Austin Chen
4faab4fcdc Clean up Featured code 2022-08-25 19:42:40 -07:00
Austin Chen
0f49effade Tweak Featured badge design 2022-08-25 19:17:50 -07:00
James Grugett
7773234138 Add debug console.log 2022-08-25 17:21:51 -05:00
Ian Philips
91bb4dfab2 With play money on numeric & center text 2022-08-25 12:06:42 -06:00
James Grugett
97b648a51e Move recommended markets below market white bg onto gray bg 2022-08-25 12:59:26 -05:00
Ian Philips
b785d4b047 With play money 2022-08-25 10:02:22 -06:00
Ian Philips
90e1fdb586 Add david to admins 2022-08-25 07:54:50 -06:00
Ian Philips
dc89d5d4d0 Feature markets on trending 2022-08-25 07:05:26 -06:00
Ian Philips
b9f0da9d3b Give all users 5 free markets 2022-08-25 05:51:56 -06:00
Ian Philips
6e3b8fdd4d Show old streak for old streak notifs 2022-08-25 05:10:38 -06:00
Ian Philips
465e219bfc Show old streak for old streak notifs 2022-08-25 05:10:24 -06:00
mantikoros
18f2550e4d resolution email: show n/a for canceled numeric 2022-08-24 23:44:30 -05:00
James Grugett
93739e7990 Fix betting streak number 2022-08-24 23:40:27 -05:00
James Grugett
51380febd4 Increase memory for updateStats function 2022-08-24 22:50:52 -05:00
James Grugett
cffd5dcd31 Weekly => daily loans 2022-08-24 22:03:07 -05:00
James Grugett
0caa5e24e8 Some other follow to watch changes 2022-08-24 21:23:12 -05:00
James Grugett
25eca71846 Convert heart to eye and follow to watch 2022-08-24 21:16:38 -05:00
Ian Philips
535e50eeac Betting streak bonus per day:10, max:50 2022-08-24 17:40:03 -06:00
Ian Philips
bca34dad60 Set max betting bonus to M 2022-08-24 17:31:35 -06:00
James Grugett
a8da5719fe Create experimental home page 2022-08-24 18:30:31 -05:00
Ian Philips
7a38d67c5b Reduce share row top margin on mobile 2022-08-24 17:11:48 -06:00
Ian Philips
7a22c7d76a Gap adjustment 2022-08-24 17:09:07 -06:00
Ian Philips
8d1cebf4db Move share button back down, small spacing tweaks 2022-08-24 17:07:22 -06:00
Ian Philips
b6e636cbc0 Small ux tweaks for signed out market page 2022-08-24 16:41:46 -06:00
mantikoros
5bf135760e fix sidebar tracking 2022-08-24 17:23:34 -05:00
SirSaltyy
74a0479cbd
Change about button (#796)
About button name change and now directs to "Help and About Center" super.so
2022-08-25 06:51:33 +09:00
Ian Philips
52a89d0783 Remove bolded More from navbar 2022-08-24 15:42:09 -06:00
Ian Philips
d553aae71e Shrink icon 2022-08-24 15:11:38 -06:00
Ian Philips
5365fa6175 💔💔💔 2022-08-24 15:09:28 -06:00
mantikoros
d5ac560f0c eslint 2022-08-24 15:36:57 -05:00
mantikoros
de74b2987a eslint 2022-08-24 15:34:00 -05:00
mantikoros
d390b39e0a eliminate fees 2022-08-24 15:29:48 -05:00
James Grugett
d6d1e8e86f Fix types 2022-08-24 13:29:35 -05:00
James Grugett
1c323c5a7f Simple recommended contracts based on contract creator 2022-08-24 12:59:23 -05:00
Ian Philips
3eb1b66e9a Lint 2022-08-24 11:58:32 -06:00
Ian Philips
c72bf506c3 Heart button on xl style 2022-08-24 11:53:29 -06:00
Ian Philips
432ee387ec Show all groups on sidebar 2022-08-24 11:23:07 -06:00
Ian Philips
a5812a5a73 Remove group chat display 2022-08-24 11:15:38 -06:00
Ian Philips
5dcaae7af6 Fix import 2022-08-24 10:51:21 -06:00
Ian Philips
480371cf9f Fix import 2022-08-24 10:50:55 -06:00
Ian Philips
f50b4775a1
Allow to follow/unfollow markets, backfill as well (#794)
* Allow to follow/unfollow markets, backfill as well

* remove yarn script edit

* add decrement comment

* Lint

* Decrement follow count on unfollow

* Follow/unfollow button logic

* Unfollow/follow => heart

* Add user to followers in place-bet and sell-shares

* Add tracking

* Show contract follow modal for first time following

* Increment follower count as well

* Remove add follow from bet trigger

* restore on-create-bet

* Add pubsub to dev.sh, show heart on FR, remove from answer trigger
2022-08-24 10:49:53 -06:00
James Grugett
78780a9219 Dedup contract leaderboards code from contract slug (merge error?) 2022-08-23 19:25:57 -05:00
Sinclair Chen
7da4eb8fe9
Fix bet modal probability sticking (#793)
* Fix button group styles
* Reset prob strike-out when bet modal closed
2022-08-23 14:31:52 -07:00
Ian Philips
bea94d58c5 Add extra text-sm 2022-08-23 07:55:26 -06:00
mantikoros
1c73d21925 weeklyMarketsEmails: send different markets to different users 2022-08-23 00:27:07 -05:00
Sinclair Chen
b476a7e3f8
Take descriptions out of LiteMarket (#789) 2022-08-22 18:18:51 -07:00
Sinclair Chen
baa27a3c85 Make Sinclair an admin 2022-08-22 17:50:59 -07:00
Sinclair Chen
20fd286756
Fix link classes duplicating on paste (#788) 2022-08-22 17:45:23 -07:00
Austin Chen
552f9add70 Reduce min time on contract graph to 1h
Allows more resolution on real-time markets, where a lot of trading happens within minutes
2022-08-22 17:23:59 -07:00
Ian Philips
3bea983662 Be more explicit after unsubscribing from weekly trending 2022-08-22 16:56:28 -06:00
Ian Philips
6929076740 Be more specific about unsubscribe 2022-08-22 16:43:08 -06:00
Ian Philips
e1775681aa Add weekly email sent flag, filter out manifold grouped markets 2022-08-22 16:36:39 -06:00
Sinclair Chen
ec4d0f6b4a
Fix notification for updated questions (#782)
* Fix update notification for question, description

* Don't notify on updated description
2022-08-22 15:26:54 -07:00
Ian Philips
b9a667b126 Add logs to weekly emails 2022-08-22 14:59:11 -06:00
mantikoros
571cf80e13 markets api: only load 500 markets by default 2022-08-22 14:42:23 -05:00
mantikoros
3313b55853 listAllContracts: sort by createdTime by default 2022-08-22 14:23:52 -05:00
Austin Chen
650aa68bcd Fix imports 2022-08-22 11:31:33 -07:00
Austin Chen
7736f1e3c1 Make duplicating better: description, closetime, logscale
Known issue: some markets like https://manifold.markets/FFSX/rojo-ronald-jones don't duplicate because too much stuff in JSON...?
2022-08-22 10:49:54 -07:00
Austin Chen
0cd61eb214 DX: Link to Firestore console from "..." 2022-08-22 10:48:21 -07:00
mantikoros
2530171721 don't run next-sitemap post build 2022-08-22 12:09:16 -05:00
mantikoros
009c85b61a listAllContracts: order by popularity score 2022-08-22 12:07:05 -05:00
mantikoros
7a0d64e72f host sitemap manually (delete nextjs sitemap) 2022-08-22 12:02:08 -05:00
mantikoros
40a22b31f3 fix sitemap 2022-08-22 11:52:05 -05:00
mantikoros
8ea9a79760 loan emoji 2022-08-22 10:31:23 -05:00
James Grugett
e6db99e810 Check loans calc for isFinite 2022-08-22 10:20:22 -05:00
Ian Philips
571d3e71b5 Only show most recent streak notif, relative econ imports, pubsub emulator 2022-08-22 06:31:30 -06:00
Austin Chen
b7790a9678 Show Referrals count for each user 2022-08-21 22:53:02 -07:00
Austin Chen
88bf678ce3
Allow custom environments to override any economic aspect (#787)
* Extract monetary constants to a single file economy.ts

* Add missing import

* Allow environments to override any econ variable

* Update imports

* Update more imports

* Fix import
2022-08-21 22:37:26 -07:00
James Grugett
8b7cd20b6f
Loans2: Bets return you some capital every week (#588)
* Remove some old loan code

* Almost complete implementation of updateLoans cloud function

* Merge fixes

* Use invested instead of sale value, check if eligible, perform payouts

* Run monday 12am

* Implement loan income notification

* Fix imports

* Loan update fixes / debug

* Handle NaN and negative loan calcs

* Working loan notification

* Loan modal!

* Move loans calculation to /common

* Better layout

* Pay back loan on sell shares

* Pay back fraction of loan on redeem

* Sell bet loan: negate buy bet's loan

* Modal tweaks

* Compute and store nextLoanCached for all users

* lint

* Update loans with newest portfolio

* Filter loans to only unresolved contracts

* Tweak spacing

* Increase memory
2022-08-22 00:22:49 -05:00
Austin Chen
3158740ea3 Minor tweaks for custom instances 2022-08-21 22:13:42 -07:00
Austin Chen
258b2a318f Default to showing weekly bet graph 2022-08-21 21:02:56 -07:00
Austin Chen
aa3647e0f3 Handle case when no charity txns 2022-08-21 20:55:04 -07:00
James Grugett
d18dd5b8fb Fix a case of limit order sorting 2022-08-21 15:58:49 -05:00
James Grugett
645cfc65f4 Update sort order of limit orders (older bets first b/c they are filled first) 2022-08-21 12:57:00 -05:00
mantikoros
97b38c156f Revert "create contract: ante no longer user liquidity provision"
This reverts commit 56e9b5fa2f.
2022-08-20 15:34:52 -05:00
mantikoros
c6dc852cd8 send creator guide on D1 2022-08-20 15:34:34 -05:00
mantikoros
ef127ea335 update welcome email 2022-08-20 15:34:34 -05:00
mantikoros
43bbc9ec24 send followup email on D2 2022-08-20 15:34:34 -05:00
Marshall Polaris
2439317408
Convert tags to groups on demand (#784)
* Fix stuff to not prematurely initialize Firebase when imported

* Add script to convert a tag to a group with the same contracts
2022-08-20 13:32:12 -07:00
James Grugett
f4ebb2b504 Fix wrapping on "Add market" button 2022-08-20 15:19:05 -05:00
Sinclair Chen
a9f846e8fc
Fix tooltip styles in your bets (#783)
* Only show answer tooltips if truncated

* Always wrap in tooltip

* refactor. make title in dialog less big
2022-08-20 13:05:33 -07:00
James Grugett
099764a931 Show unlisted markets in groups and under your markets 2022-08-20 14:38:15 -05:00
James Grugett
09e8993cd4 Implement visibility option for new markets 2022-08-20 14:31:32 -05:00
mantikoros
dd6c5dc97a betting streaks copy 2022-08-20 13:47:26 -05:00
James Grugett
2fef413d88 Don't show fantasy football in newest sort 2022-08-20 13:46:14 -05:00
James Grugett
474304d284 Revert "🔥🔥🔥🔥🔥🔥🔥🔥"
This reverts commit fc8487dca0.
2022-08-20 11:45:13 -05:00
Austin Chen
6791da0fc8 Fix "Most Recent Donor" on /charity 2022-08-19 17:28:08 -07:00
Sinclair Chen
c850cfe97f Revert "Revert "fix firefox visual glitch - single card wrapping""
This reverts commit 63a5241b2e.
2022-08-19 16:59:42 -07:00
Austin Chen
51c843d765
Use masonry on contract cards, sorted correctly (#773)
* Revert "Revert "Tile contract cards in masonry layout (#761)""

This reverts commit 62728e52b7.

* Sort the contracts in the correct masonry order

* Fix ordering on single columns

* Use react-masonry-css to accomplish masonry view

* Improve comment

* Remove gridClassName

Everything is spaced with m-4, too bad
2022-08-19 16:57:23 -07:00
Ian Philips
03d98a7ad7 Reset hour to 12am utc 2022-08-19 17:16:17 -06:00
mantikoros
0cbc0010c1 schedule emails from onCreateUser; send interesting markets on D1 2022-08-19 17:03:00 -05:00
Ian Philips
fc8487dca0 🔥🔥🔥🔥🔥🔥🔥🔥 2022-08-19 16:00:40 -06:00
James Grugett
b67a26ad61 Don't show bets streak modal on navigate each tab 2022-08-19 16:51:52 -05:00
Ian Philips
39c312cf9f Explicitly pass utc timezone 2022-08-19 15:19:52 -06:00
Ian Philips
1196ec4375 Send 6 trending emails to all users monday 12pm PT 2022-08-19 15:01:53 -06:00
Ian Philips
634196d8f1 Slice the popular emails to the top 20 2022-08-19 14:45:04 -06:00
Ian Philips
36bfbe8f42 Change betting streak modal, tweak trending email query 2022-08-19 14:37:16 -06:00
Ian Philips
a0f62ba172
Markets emails (#764)
* Send out email template for 3 trending markets

* Rich text to plaintext descriptions, other ui changes

* Lint

* Filter for closed markets

* Change sign

* First order must be closeTime

* Send 6 emails, check flag twice

* Exclude contracts with trump and president in the name

* interesting markets email

* sendInterestingMarketsEmail

* Change subject line back

Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-19 11:43:57 -06:00
Ian Philips
ba5dabd613 Increase gap between profit and streak 2022-08-19 11:24:28 -06:00
Ian Philips
00c9fa61c3
betting streaks (#777)
* Parse notif, show betting streaks modal, schedule function

* Ignore streaks of 0

* Pass notifyFills the contract

* Turn 9am into a constant

* Lint

* Up streak reward, fix timing logic

* Change wording
2022-08-19 11:10:32 -06:00
Sinclair Chen
4f3202f90b
Simple bet interface in embeds (#775)
* rename BetRow -> BetButton

* Replace bet modal in embed with inline betting

- Also simplifies graph height calculation

* Move bet row above graph, in "mini modal"

* Show signup button if not signed up

* Show probability change

* Show error after modal

- Show balance if insufficient funds
- Clear error from amount input if amount deleted entirely

* Fix error state conditions

- Reset amount input on success
- Reset success state on user input

* Make input smaller (80px)
2022-08-19 10:07:48 -07:00
Sinclair Chen
98a0ed99c9 Fix (i) alignment 2022-08-19 09:53:16 -07:00
Sinclair Chen
4d7df00a68
Make Avatar component update when avatarUrl updates (#780) 2022-08-19 09:47:00 -07:00
Barak Gila
f51ad2224b
add YIMBY Law and CaRLA as charities (#781) 2022-08-19 10:52:01 -05:00
Marshall Polaris
0972de9025
Make typing for comments more fancy (#776) 2022-08-19 01:06:40 -07:00
Sinclair Chen
f2764e9258
Remove keyboard accessibility for tooltips (#779)
Headless UI's Modal component autofocuses the first focusable item
inside it when opened. This is by design for accessibility reasons.
See https://headlessui.com/react/dialog#managing-initial-focus

Ironically this means we'll have to remove keyboard focus for tooltips
because this causes the tooltips to pop up unnecessarily for all users
whenever the dialog is opened. The alternative is managing focus
manually for several dialogs, which may not be possible as some of our
modals lack a sensible element to focus by default.
2022-08-18 18:54:09 -07:00
Sinclair Chen
2537663a57
Fix user avatar in mention list not updating (#778) 2022-08-18 17:20:40 -07:00
Marshall Polaris
0cf9a90cfb
Remove some dead code related to tags, categories, and old feed stuff (#765)
* Remove dead image storage code

* Kill tag page

* Kill tag and categories related component UI

* Kill some old algo feed kind of code
2022-08-18 15:46:11 -07:00
Keri Warr
4f6d478211
List manifold-sdk on the Awesome Manifold page (#774) 2022-08-18 16:59:18 -05:00
Marshall Polaris
06ced7042d Fix a typo in my script 2022-08-18 12:49:01 -07:00
Marshall Polaris
c37997bcb7
Add comment type field to comments (#772) 2022-08-18 12:47:35 -07:00
Austin Chen
c2db558b85 Describe why subsidizing is good 2022-08-18 10:12:38 -07:00
James Grugett
097000c9da Don't scroll to top on search change except on home 2022-08-18 11:23:16 -05:00
mantikoros
d216b298ba "create-contract.ts" => "create-market.ts" 2022-08-18 11:14:16 -05:00
mantikoros
56e9b5fa2f create contract: ante no longer user liquidity provision 2022-08-18 11:14:16 -05:00
Ian Philips
c9c3a95d2a Condense user profile bits 2022-08-18 09:54:30 -06:00
mantikoros
87561503c1 accept challenge: redeem shares 2022-08-18 10:39:48 -05:00
Ian Philips
68a949de35 Change Challenge page wording 2022-08-18 08:22:37 -06:00
Ian Philips
33edd3c0fb
Create challenge without previous market (#718)
* Create challenge without previous market

* Check if they've balance to create both on fe

* Change wording slightly

* Finish merge
2022-08-18 08:15:20 -06:00
Ian Philips
c3d09e5323 Add links to challenge page 2022-08-18 07:53:19 -06:00
Marshall Polaris
97fa5fa636
Replace /markets with /home (#766)
* Make /home not kick out logged out users

* Point people at /home instead of /markets
2022-08-17 23:15:25 -07:00
marsteralex
fb67010c0e
include draft innovation basics (#771)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

* include battlebond

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 18:17:29 -07:00
marsteralex
5bf39a7a92
sort by set name (#770)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

* sort names by set instead of by set symbol

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 18:01:59 -07:00
marsteralex
2c97be815b
remove check from original (#768)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

* removed check from original

* remove check from original

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 16:03:02 -07:00
mantikoros
159723ed0c market creation email 2022-08-17 17:36:52 -05:00
marsteralex
ce3d092497
Add Basic Lands to MTG Guesser (#716)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

* add basic land guesser

also added fetcher to filter all cards instead of only unique art

* default to original

makes basic better

* added set symbol to basics

added set symbol to the basics game mode. Changed name to "How Basic"

* cleanup

* changed some pixels

* only load set data if needed

* hacked fix for removing image from name

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-08-17 12:40:59 -07:00
Sinclair Chen
99009f841b Make text of old chats same size as current 2022-08-17 10:45:30 -07:00
mantikoros
bf64f5b3a9 redirect /about; more content in welcome email 2022-08-17 11:21:29 -05:00
James Grugett
770a8d049c Check limit prob with floating equals 2022-08-17 10:55:47 -05:00
Ian Philips
a00857cb45 Fix wrapping close date and truncate group name 2022-08-16 16:03:55 -06:00
Sinclair Chen
59565416b6
Api fixes (#704)
* Add min, max, isLogScale to numeric market API return

* Add lastUpdatedTime to market API

* Return a string description in market API

* Accept string descriptions in market POST api

* install prettier eslint config. fix import

* fix another import
2022-08-16 15:01:03 -07:00
James Grugett
8c2f3c56d3 Limit orders: Subtract fees from "profit if both filled" 2022-08-16 15:51:04 -05:00
James Grugett
814c4aa01d Change limit prob validation to be only on Binary markets (not numeric) 2022-08-16 15:44:58 -05:00
James Grugett
62728e52b7 Revert "Tile contract cards in masonry layout (#761)"
This reverts commit 4002c23bee.
2022-08-16 13:03:04 -05:00
James Grugett
63a5241b2e Revert "fix firefox visual glitch - single card wrapping"
This reverts commit ec7263da18.
2022-08-16 13:02:55 -05:00
mantikoros
c58ed8bd2c personal followup email 2022-08-16 11:45:58 -05:00
mantikoros
c3eaf0351b one week email changes 2022-08-16 11:44:01 -05:00
Marshall Polaris
59ca1f7640
Denormalize some contract comment fields (#760)
* Make `groupConsecutive` more capable

* Put denormalized `contractQuestion` and `contractSlug` on comments

* Update user profile UI to use new denormalized fields

* `/Austin` -> `/market`
2022-08-15 22:43:46 -07:00
Marshall Polaris
d00fe7bcd2
Backend robustness to email sending or analytics tracking failures (#728)
* Make `sendEmail` functions await email send success

* Make tracking and email sending not throw on failure
2022-08-15 22:13:38 -07:00
Marshall Polaris
186befd0ac
Bail out earlier if createmarket is called with invalid group ID (#745)
* Bail out earlier if `createmarket` is called with invalid group ID

* Fix typing in `createmarket`
2022-08-15 22:12:43 -07:00
Sinclair Chen
ec7263da18 fix firefox visual glitch - single card wrapping 2022-08-15 22:08:09 -07:00
James Grugett
f2f77cb51e Resolve market emails: fix negative amount bug with better invested calculation 2022-08-15 21:48:01 -05:00
James Grugett
e5aef763cd Calculate invested properly for DPM 2022-08-15 21:48:01 -05:00
James Grugett
aef14e49bb Update bet type to explain dpm props 2022-08-15 21:48:01 -05:00
Ian Philips
cd520e6cfe lint 2022-08-15 19:47:58 -06:00
Ian Philips
d56435b9cd Signed out home page shows dynamic trending markets 2022-08-15 19:34:45 -06:00
Sinclair Chen
4002c23bee
Tile contract cards in masonry layout (#761) 2022-08-15 17:41:53 -07:00
James Grugett
997d68a574 Compute invested & display in your bets 2022-08-15 19:04:37 -05:00
Austin Chen
34e8138e50 Show placeholder when avatarUrl errors 2022-08-15 16:33:02 -07:00
Ian Philips
428d9a3692 Move avatar to below card on mobile 2022-08-15 13:49:33 -06:00
James Grugett
2ff2d6c1fc Scroll to top for fresh query 2022-08-15 14:26:18 -05:00
mantikoros
5c49461449 new welcome email 2022-08-15 11:12:33 -05:00
James Grugett
c80f82a3f7 Home page hack: discard NextJS router state 2022-08-15 11:06:42 -05:00
Marshall Polaris
972f215f0c
Rewrite useQueryAndSortParams machinery to be faster/simpler/better (#758)
* Rewrite useQueryAndSortParams machinery to be faster/simpler/better

* Politely debounce Algolia querying

* Tidy some stuff up

* Style changes suggested by James
2022-08-14 22:09:25 -07:00
mantikoros
5d14d79e6e share dialog: remove native sharer; use toast for embed 2022-08-15 00:03:05 -05:00
mantikoros
b57c84bbd9 notifications title/seo 2022-08-14 23:55:11 -05:00
James Grugett
4e1fae5b5f Require a whole percentage for limitProb in back end 2022-08-14 20:51:10 -05:00
Marshall Polaris
0b711be480
Clean up a bunch of markup and CSS on contract cards (#753)
* Remove random unnecessary top-level divs

* Remove wrapper in MiscDetails

* Remove another wrapper in ContractCard

* Fix a bunch of weird CSS stuff
2022-08-14 01:05:17 -07:00
Marshall Polaris
69c49679f1
Move search controls into separate component (#757)
* Move search controls into separate component

* Fix up typing on pill groups thingy

* More precise comparison per James

* Make sure `additionalFilter` is passed into controls
2022-08-13 16:34:03 -07:00
Marshall Polaris
0085ffcb0b
Simplify and fix inefficiencies in contract search component (#756)
* Simplify and fix inefficiencies in contract search component

* Add react-dom types

* Add a clarifying comment

* Improve search per some feedback
2022-08-13 13:15:11 -07:00
James Grugett
0a9df3ac6b Group horizontal margin on tabs 2022-08-13 13:50:26 -05:00
James Grugett
aeea66491a Group question => market 2022-08-13 13:49:25 -05:00
Marshall Polaris
456d9398a1
Revamp a lot of stuff on the user page to make it usably efficient (#751)
* Load bets and comments tabs data on user page independently

* Implement basic pagination on profile comments list

* Tweak server auth to return `null` instead of `undefined`

* Switch to SSR for user page

* Fix lint

* Fix broken contract fetching in user bets list

* Tidying
2022-08-12 20:42:58 -07:00
Sinclair Chen
dcc3c61f52
Only calculate position when tooltip is shown (#755) 2022-08-12 20:35:08 -07:00
Marshall Polaris
0f7f55ec0a Fix embarrassing bug in server auth 2022-08-12 20:14:24 -07:00
Marshall Polaris
e4239d0122
Tweak Firestore user rules to be more robust (#750) 2022-08-12 20:13:09 -07:00
Sinclair Chen
facb19a347
fix dependency peer-dep warnings, mostly (#752) 2022-08-12 17:49:08 -07:00
Marshall Polaris
96a378ec4b
Make RelativeTimestamp a little more efficient (#754)
* Don't do extra dayjs work in timestamp components

* Remove extra wrapper from `RelativeTimestamp`
2022-08-12 17:48:41 -07:00
Marshall Polaris
79be0c555b Fix tiny bug in auth context code 2022-08-12 13:45:38 -07:00
Marshall Polaris
3cb28cdecb
Teach AuthContext to manage the private user doc (#738)
* Return both user and privateUser from `createuser`

* Make `useStateCheckEquality` more flexible

* Make `AuthContext` track the private user doc

* Change `usePrivateUser` hook to use the auth context data

* Pass both user and private user through SSR to auth context

* Fix bug in create user flow
2022-08-12 13:41:00 -07:00
James Grugett
3cbf5a6f7d Always show search placeholder 2022-08-12 14:35:27 -05:00
Sinclair Chen
20ab313c6c Improve profile comments vis d 2022-08-12 12:10:45 -07:00
Sinclair Chen
88535e5512 fix lint error 2022-08-12 12:10:07 -07:00
Sinclair Chen
df858f916b
Migrate daisy tooltips to our own to fix cutoffs (#748)
* Make all tooltips use our component

* Stop mobile tooltip crop (daisy -> floating-ui)

* Show tooltip on tap for touch devices

Except tooltips on buttons

* migrate another daisy tooltip to ours

* Prevent hidden tooltip from covering click/hover
2022-08-12 12:04:23 -07:00
mantikoros
d2b634c775 template email tracking 2022-08-12 11:33:02 -05:00
mantikoros
8ebccd05ec market movement warning; add bankroll warning to FR markets 2022-08-12 11:24:08 -05:00
Marshall Polaris
80fd38990f Experimentally do not optimizeCss 2022-08-11 21:07:54 -07:00
Austin Chen
7ad8af848a Replace DaisyUI buttons with TailwindUI buttons
Maybe this should use the button component...? But that's styled differently, the rest of /create uses standard TailwindUI
2022-08-11 20:54:12 -07:00
Marshall Polaris
e2eae01ad8
Add a shitload of logging to the server auth code (#749) 2022-08-11 20:46:18 -07:00
Marshall Polaris
38d9e8190c
Only load portfolio history inside user page bets tab (#747) 2022-08-11 20:44:51 -07:00
Austin Chen
af4c442105 Support Twitch video and channel embeds 2022-08-11 20:23:33 -07:00
Austin Chen
9311652bed
Support Youtube, Tweet, and Metaculus embeds in editor (#744)
* Embed a tweet by URL

* Clean up imports

* Prevent tweetId from getting interpreted as a number

* Use a single place to embed iframe, Youtube, and Tweets

* Support Manifold and Metaculus embeds

* Remove unused import

* Simplify Manifold paste logic

* Clean up embed rewrite code

* Add back comment

* Rejigger config so tsx is only in web/

* Clean up comment

* Revert unnecessary tsconfig change

* Fix placeholder

* Lighten placeholder
2022-08-11 20:18:01 -07:00
Sinclair Chen
daba28423a
Improve create page UI (#746)
* Adjust create page styles

* Keep answers when switching market type
2022-08-11 14:41:21 -07:00
Austin Chen
dc95587cca
Add editor toolbar to choose and embed markets (#702)
* Embed markets using the "add markets" template

* Override dev domain

* Improve market modal style

- contract searchbar now sticky
- entire card clickable to select (if quickbet is hidden)
- adjust selected card styles

* remove extra export

* Hide pills

* Fix browser redirect warning

* Insert all markets instead of just one

* fix type error

* fixup "Insert all markets instead of just one"

Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
2022-08-11 14:32:02 -07:00
Marshall Polaris
4e8b94a28c
Componentize confetti to isolate re-renders due to window size (#740)
* Componentize confetti to isolate re-renders due to window size

* Clean up debug logging
2022-08-11 12:55:25 -07:00
Marshall Polaris
b9f347b7f4
Use UserFollowButton instead of FollowButton in UserPage (#742) 2022-08-11 12:54:09 -07:00
Marshall Polaris
ad75ecdc87
Move liquidity provision fetch down into ContractTabs (#741) 2022-08-11 12:53:54 -07:00
Marshall Polaris
daa86fa330
Change tabs to keep all individual tab components in the DOM (#743) 2022-08-11 12:53:42 -07:00
Sinclair Chen
99326eb65a fix spacing of long group names on markets 2022-08-11 12:30:34 -07:00
James Grugett
3f6ca6c8ed Make Manifold account able to resolve markets 2022-08-11 00:38:15 -05:00
James Grugett
6e93f11a59 Fix bolded group chat not getting unbolded 2022-08-11 00:00:40 -05:00
James Grugett
61ae481a03 Document cancel bet 2022-08-10 18:43:11 -05:00
Marshall Polaris
8c537537a1
Add cache headers to avatars (#737)
* Set cache headers on newly uploaded avatars

* Go fix up all the old avatars to have cache headers
2022-08-10 11:03:55 -07:00
Austin Chen
b5b77be188 Accept URLs in the iframe editor
TODO: Update placeholder text to mention this
2022-08-10 11:03:39 -07:00
James Grugett
d7b021b79f Clear entered limit probs on submit limit order 2022-08-10 12:37:51 -05:00
James Grugett
654790315c Fix missing key console error 2022-08-10 12:33:29 -05:00
mantikoros
35df201e2e prob bar for multiple choice 2022-08-10 12:32:30 -05:00
SirSaltyy
e591de8b29
Increase description max length (#739) 2022-08-11 02:31:28 +09:00
James Grugett
4d953d58a1 Move group chat back into a tab 2022-08-10 12:28:02 -05:00
mantikoros
dc26db2864 add salem to sidebar; clean up code 2022-08-10 12:17:40 -05:00
jahooma
3d30a1adbc Auto-prettification 2022-08-10 17:06:34 +00:00
James Grugett
05c9d3513a Don't reference window outside useEffect or click event. 2022-08-10 12:05:56 -05:00
mantikoros
52a2a3d842 track search 2022-08-10 11:50:21 -05:00
Marshall Polaris
521c479abf Fix an embarrassing bug in getPrivateUser 2022-08-09 23:55:41 -07:00
Marshall Polaris
818c90a95e
Refactor tipper (#734)
* Clean up tipping components

* Pass comment into tip callback
2022-08-09 23:05:56 -07:00
mantikoros
5f77a026aa fix modal 2022-08-09 21:59:40 -05:00
mantikoros
63538ae925 referral link on user page, manalinks, market share dialog; native sharer on mobile 2022-08-09 21:51:08 -05:00
Sinclair Chen
0b9ca6b7ee
Editor improvements (#735)
* Allow focus on all parts of editor

* Fix background and text colors

* Restrict height of image in comment

* Remove "Type *markdown*" placeholder

it's a little misleading (can't do markdown links) and messes with focus

to be replaced with a highlight menu in the future
2022-08-09 19:04:55 -07:00
Marshall Polaris
c07daafb8d
Make homepage load user via SSR, pass it to contract stuff (#729) 2022-08-09 15:28:52 -07:00
Marshall Polaris
847d3d0f27
Fix efficiency problems with visibility checking code (#730)
* Fix problems with visibility checking code

* Tear out old contract tracking stuff per James

* Use `useEvent` in VisibilityObserver per James suggestion
2022-08-09 15:28:27 -07:00
Marshall Polaris
5715b0e44a
Random contract page fixups (#712)
* Remove some divs and so on

* Correctly align bet avatars and text in feed

* Extract sidebar component on contract page
2022-08-09 13:25:42 -07:00
mantikoros
1e3c5cb936 add qr code to referrals 2022-08-09 12:27:52 -05:00
Sinclair Chen
914fc476ce
Remove top/bottom margin from indented list items (#733) 2022-08-09 10:17:44 -07:00
Austin Chen
49541d3eec Stop interpolating on portfolio value graph 2022-08-09 10:08:14 -07:00
Marshall Polaris
592125b5e7
Fix broken useBets filters (#731) 2022-08-09 10:50:11 -05:00
Marshall Polaris
e7f1d3924b
Fix up several pages to load user data on the server (#722)
* Fix up several pages to load user data on the server

* Add key prop to `EditUserField`
2022-08-08 22:43:04 -07:00
Marshall Polaris
5649161348
Pass page props user to auth provider if present (#724)
* Pass page props user to auth provider if present

* Rename `user` -> `serverUser`

* Don't load from local storage if server told us a user
2022-08-08 22:42:52 -07:00
Austin Chen
fd308151b3 Disable bouncing Challenge 2022-08-08 15:24:30 -07:00
Marshall Polaris
85e55312ca
What will be removed, is removed (#721) 2022-08-08 15:05:25 -07:00
James Grugett
98806a806f Fix query params on emulator/private instance 2022-08-07 18:07:36 -07:00
James Grugett
8fb3b42ea1 Default to trending. Fix close date being opposite 2022-08-07 17:48:43 -07:00
Austin Chen
a910e5dc17 Revert "Revert "Fix a bug with expiration of refresh and custom tokens""
This reverts commit 012b67e3c5.
2022-08-07 09:57:18 -07:00
Austin Chen
012b67e3c5 Revert "Fix a bug with expiration of refresh and custom tokens"
This reverts commit abd344b951.
2022-08-07 09:56:42 -07:00
Marshall Polaris
abd344b951 Fix a bug with expiration of refresh and custom tokens 2022-08-06 19:24:50 -07:00
James Grugett
1f8aef2891 Disable challenges for private instances 2022-08-06 17:45:21 -07:00
Sinclair Chen
da977f62a9
Make added text go after img instead of replacing (#725) 2022-08-06 15:43:41 -07:00
Sinclair Chen
5892ccee97
Rich text in comments, fixed (#719)
* Revert "Revert "Switch comments/chat to rich text editor (#703)""

This reverts commit 33906adfe4.

* Fix typing after being weird on Android

Issue: character from mention gets deleted. Most weird on Swiftkey:
mention gets duplicated instead of deleting.

See https://github.com/ProseMirror/prosemirror/issues/565
https://bugs.chromium.org/p/chromium/issues/detail?id=612446

The keyboard still closes unexpectedly when you delete :(

* On reply, put space instead of \n after mention

* Remove image upload from placeholder text

- Redundant with image upload button
- Too long on mobile

* fix dependency
2022-08-06 13:39:52 -07:00
Marshall Polaris
d43b9e1836
Vercel auth phase 2 (#714)
* Add cloud function to get custom token from API auth

* Use custom token to authenticate Firebase SDK on server

* Make sure getcustomtoken cloud function is fast

* Make server auth code maximally robust

* Refactor cookie code, make set and delete more flexible
2022-08-05 20:49:29 -07:00
James Grugett
e0196f7107 Rename file contracts-list to contracts-group 2022-08-05 17:46:32 -07:00
James Grugett
b3b06896be Add loading indicator when algolia search is initially loading 2022-08-05 17:44:55 -07:00
Marshall Polaris
48ac21ffad Add comment explaining fishy token 2022-08-05 16:08:30 -07:00
Marshall Polaris
bf3ba8ac3f Remove test file 2022-08-05 16:07:02 -07:00
mqp
bba9f9a555 Auto-prettification 2022-08-05 23:03:25 +00:00
Marshall Polaris
7e0634aee0 Try using a personal access token for formatter job 2022-08-05 16:02:46 -07:00
Marshall Polaris
f05db0ef0f Give formatting workflow even more permissions... 2022-08-05 15:56:10 -07:00
Marshall Polaris
db3b0c2cf5 Give repo write permission to formatting workflow 2022-08-05 15:38:22 -07:00
Marshall Polaris
d9ddabcfd4 Commit some un-pretty code 2022-08-05 15:35:57 -07:00
Marshall Polaris
67139b99f9
Add workflow to automate prettier running on main (#720) 2022-08-05 15:33:34 -07:00
mantikoros
5e89628593 challenge bet tracking 2022-08-05 13:42:09 -07:00
mantikoros
f11c9a3341 bouncing challenge button (temporary gimmick) 2022-08-05 13:42:09 -07:00
James Grugett
ced404eb74 Local search filters on groups, exclude contractIds 2022-08-05 12:01:16 -07:00
Ian Philips
60ebadbbe5 Add not about donating winnings to charity 2022-08-05 09:58:02 -06:00
Ian Philips
f47b70dd3c Darken numeric preview text 2022-08-05 07:08:41 -06:00
Ian Philips
de6d5b388a Lint 2022-08-05 06:58:39 -06:00
Ian Philips
1c80bf1faf Chat icon => users icon 2022-08-05 06:58:29 -06:00
Ian Philips
97e3de4e0f Show numeric values in card preview 2022-08-05 06:56:10 -06:00
Ian Philips
d90901b4e3 Check creator balance again upon acceptance 2022-08-05 05:03:47 -06:00
mantikoros
f3704633ee liquidity panel styling 2022-08-05 00:03:38 -07:00
mantikoros
5988dd1e48 improved create challenge modal; 2xs button 2022-08-04 23:42:35 -07:00
mantikoros
16f4fb9490 disable clicking on group in embed 2022-08-04 22:47:59 -07:00
mantikoros
4d153755c1 delete challenge button 2022-08-04 22:33:56 -07:00
mantikoros
1e66f4d140
Share row (#715)
* Challenge bets

* Store avatar url

* Fix before and after probs

* Check balance before creation

* Calculate winning shares

* pretty

* Change winning value

* Set shares to equal each other

* Fix share challenge link

* pretty

* remove lib refs

* Probability of bet is set to market

* Remove peer pill

* Cleanup

* Button on contract page

* don't show challenge if not binary or if resolved

* challenge button (WIP)

* fix accept challenge: don't change pool/probability

* Opengraph preview [WIP]

* elim lib

* Edit og card props

* Change challenge text

* New card gen attempt

* Get challenge on server

* challenge button styling

* Use env domain

* Remove other window ref

* Use challenge creator as avatar

* Remove user name

* Remove s from property, replace prob with outcome

* challenge form

* share text

* Add in challenge parts to template and url

* Challenge url params optional

* Add challenge params to parse request

* Parse please

* Don't remove prob

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Add to readme about how to dev og-image

* Add emojis

* button: gradient background, 2xl size

* beautify accept bet screen

* update question button

* Add separate challenge template

* Accepted challenge sharing card, fix accept bet call

* accept challenge button

* challenge winner page

* create challenge screen

* Your outcome/cost=> acceptorOutcome/cost

* New create challenge panel

* Fix main merge

* Add challenge slug to bet and filter by it

* Center title

* Add helper text

* Add FAQ section

* Lint

* Columnize the user areas in preview link too

* Absolutely position

* Spacing

* Orientation

* Restyle challenges list, cache contract name

* Make copying easy on mobile

* Link spacing

* Fix spacing

* qr codes!

* put your challenges first

* eslint

* Changes to contract buttons and create challenge modal

* Change titles around for current bet

* Add back in contract title after winning

* Cleanup

* Add challenge enabled flag

* Spacing of switch button

* market share row

* Add lite market endpoint

* 500 mana email (#687)

* Create 500-mana.html

* Update 500-mana.html

Fixed typos and links not working

* Added "create a good market" guide

added page creating-market.html
For Stephen to set up condition (email 3 days after signing up)

* Update 500-mana.html

updated 500 Mana email (still need to make changes to create market guide)

* email changes

* sendOneWeekBonusEmail logic

* add dayjs as dependency

* don't use mailgun scheduling

Co-authored-by: mantikoros <sgrugett@gmail.com>

* Challenge Bets (#679)

* Challenge bets

* Store avatar url

* Fix before and after probs

* Check balance before creation

* Calculate winning shares

* pretty

* Change winning value

* Set shares to equal each other

* Fix share challenge link

* pretty

* remove lib refs

* Probability of bet is set to market

* Remove peer pill

* Cleanup

* Button on contract page

* don't show challenge if not binary or if resolved

* challenge button (WIP)

* fix accept challenge: don't change pool/probability

* Opengraph preview [WIP]

* elim lib

* Edit og card props

* Change challenge text

* New card gen attempt

* Get challenge on server

* challenge button styling

* Use env domain

* Remove other window ref

* Use challenge creator as avatar

* Remove user name

* Remove s from property, replace prob with outcome

* challenge form

* share text

* Add in challenge parts to template and url

* Challenge url params optional

* Add challenge params to parse request

* Parse please

* Don't remove prob

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Add to readme about how to dev og-image

* Add emojis

* button: gradient background, 2xl size

* beautify accept bet screen

* update question button

* Add separate challenge template

* Accepted challenge sharing card, fix accept bet call

* accept challenge button

* challenge winner page

* create challenge screen

* Your outcome/cost=> acceptorOutcome/cost

* New create challenge panel

* Fix main merge

* Add challenge slug to bet and filter by it

* Center title

* Add helper text

* Add FAQ section

* Lint

* Columnize the user areas in preview link too

* Absolutely position

* Spacing

* Orientation

* Restyle challenges list, cache contract name

* Make copying easy on mobile

* Link spacing

* Fix spacing

* qr codes!

* put your challenges first

* eslint

* Changes to contract buttons and create challenge modal

* Change titles around for current bet

* Add back in contract title after winning

* Cleanup

* Add challenge enabled flag

* Spacing of switch button

* Put sharing qr code  in modal

Co-authored-by: mantikoros <sgrugett@gmail.com>

* See challenges you've accepted too

* Remove max height

* Notify mentioned users on market publish (#683)

* Add function to parse at mentions

* Notify mentioned users on market create

- refactor createNotification to accept list of recipients' ids

* Switch comments/chat to rich text editor (#703)

* Switch comments/chat to rich text editor

* Remove TruncatedComment

* Re-add submit on enter

* Insert at mention on reply

* Update editor style for send button

* only submit on enter in chat

* code review: refactor

* use more specific type for upload

* fix ESlint and errors from merge

* fix trigger on every render eslint warning

* Notify people mentioned in comment

* fix type errors

* Revert "Switch comments/chat to rich text editor (#703)"

This reverts commit f52da72115.

* merge conflict

* share modal

* merge issue

* eslint

* bigger link icion

Co-authored-by: Ian Philips <iansphilips@gmail.com>
Co-authored-by: James Grugett <jahooma@gmail.com>
Co-authored-by: SirSaltyy <104849031+SirSaltyy@users.noreply.github.com>
Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
2022-08-05 00:22:45 -05:00
James Grugett
33906adfe4 Revert "Switch comments/chat to rich text editor (#703)"
This reverts commit f52da72115.
2022-08-04 16:49:59 -07:00
Sinclair Chen
f52da72115
Switch comments/chat to rich text editor (#703)
* Switch comments/chat to rich text editor

* Remove TruncatedComment

* Re-add submit on enter

* Insert at mention on reply

* Update editor style for send button

* only submit on enter in chat

* code review: refactor

* use more specific type for upload

* fix ESlint and errors from merge

* fix trigger on every render eslint warning

* Notify people mentioned in comment

* fix type errors
2022-08-04 16:34:04 -07:00
Sinclair Chen
edae709f5f
Notify mentioned users on market publish (#683)
* Add function to parse at mentions

* Notify mentioned users on market create

- refactor createNotification to accept list of recipients' ids
2022-08-04 15:35:55 -07:00
Ian Philips
912ccad530 Remove max height 2022-08-04 16:09:33 -06:00
Ian Philips
c93f9c5483 See challenges you've accepted too 2022-08-04 15:58:48 -06:00
Ian Philips
798253f887
Challenge Bets (#679)
* Challenge bets

* Store avatar url

* Fix before and after probs

* Check balance before creation

* Calculate winning shares

* pretty

* Change winning value

* Set shares to equal each other

* Fix share challenge link

* pretty

* remove lib refs

* Probability of bet is set to market

* Remove peer pill

* Cleanup

* Button on contract page

* don't show challenge if not binary or if resolved

* challenge button (WIP)

* fix accept challenge: don't change pool/probability

* Opengraph preview [WIP]

* elim lib

* Edit og card props

* Change challenge text

* New card gen attempt

* Get challenge on server

* challenge button styling

* Use env domain

* Remove other window ref

* Use challenge creator as avatar

* Remove user name

* Remove s from property, replace prob with outcome

* challenge form

* share text

* Add in challenge parts to template and url

* Challenge url params optional

* Add challenge params to parse request

* Parse please

* Don't remove prob

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Challenge card styling

* Add to readme about how to dev og-image

* Add emojis

* button: gradient background, 2xl size

* beautify accept bet screen

* update question button

* Add separate challenge template

* Accepted challenge sharing card, fix accept bet call

* accept challenge button

* challenge winner page

* create challenge screen

* Your outcome/cost=> acceptorOutcome/cost

* New create challenge panel

* Fix main merge

* Add challenge slug to bet and filter by it

* Center title

* Add helper text

* Add FAQ section

* Lint

* Columnize the user areas in preview link too

* Absolutely position

* Spacing

* Orientation

* Restyle challenges list, cache contract name

* Make copying easy on mobile

* Link spacing

* Fix spacing

* qr codes!

* put your challenges first

* eslint

* Changes to contract buttons and create challenge modal

* Change titles around for current bet

* Add back in contract title after winning

* Cleanup

* Add challenge enabled flag

* Spacing of switch button

* Put sharing qr code  in modal

Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-04 15:27:02 -06:00
SirSaltyy
2d3ca47b52
500 mana email (#687)
* Create 500-mana.html

* Update 500-mana.html

Fixed typos and links not working

* Added "create a good market" guide

added page creating-market.html
For Stephen to set up condition (email 3 days after signing up)

* Update 500-mana.html

updated 500 Mana email (still need to make changes to create market guide)

* email changes

* sendOneWeekBonusEmail logic

* add dayjs as dependency

* don't use mailgun scheduling

Co-authored-by: mantikoros <sgrugett@gmail.com>
2022-08-04 13:03:02 -05:00
James Grugett
7e46188107 Add lite market endpoint 2022-08-03 22:21:22 -07:00
Ian Philips
d83e103fab Ignore clicks when hidden 2022-08-03 18:42:40 -06:00
Ian Philips
5bc905b358 Bottom padding works on mobile, broken on desktop :( 2022-08-03 16:42:51 -06:00
Ian Philips
b4c6b99e09 Remove bottom bar height correction 2022-08-03 16:38:00 -06:00
Ian Philips
756115ba54 Link tags aren't hiding sidebar on click 2022-08-03 16:30:05 -06:00
Ian Philips
fab83cfc33 Don't auotfocus on mobile 2022-08-03 16:16:46 -06:00
Ian Philips
aa3101baa9 Fix group chat padding 2022-08-03 16:10:02 -06:00
Ian Philips
82419d0b92
Groups chat ux (#713)
* Add in group chat bubble

* Show chat bubble on nav with unseen notifs

* Spacing

* More spacing

* Remove chat tab

* Show chat on help/welcome/updates/features groups

* Cleanup

* Scroll with updated height
2022-08-03 15:38:35 -06:00
James Grugett
a7d80d62cb Don't show cancel button for other people's limit orders 2022-08-03 14:30:59 -07:00
James Grugett
a761f8c65e Hide pills while searching 2022-08-03 14:17:45 -07:00
James Grugett
280308b625 Show # of bets equal to visible bets 2022-08-02 17:40:34 -07:00
James Grugett
b5d8acfef3 Switch profit in bets tab to match user page 2022-08-02 17:31:49 -07:00
James Grugett
3c9108de0d Document creating a limit order in API 2022-08-02 17:01:31 -07:00
Ian Philips
c24b4e77a8 Lint 2022-08-02 17:24:59 -06:00
James Grugett
d45edb7887 Add WagerWith.me and James' Bot to Awesome Manifold 2022-08-02 16:21:07 -07:00
Ian Philips
e700697423 Fix group referrals not working 2022-08-02 17:18:08 -06:00
Marshall Polaris
f8a74aa438
Allow admins to resolve any market (#711) 2022-08-02 15:34:20 -07:00
mantikoros
6563082746 move claim button 2022-08-02 15:22:53 -07:00
mantikoros
5e8b9711dc hide pagination if only one page 2022-08-02 15:22:53 -07:00
mantikoros
96c0876053 manalinks: fix focus 2022-08-02 15:22:53 -07:00
mantikoros
164d9ef079 manalinks: mention referral bonus 2022-08-02 15:22:53 -07:00
Austin Chen
53d89fa4ac Show the value to 2 decimal places on hover 2022-08-02 14:59:47 -07:00
Marshall Polaris
b83caf4dd9 Just make me endpoint forward the backend response 2022-08-02 00:21:51 -07:00
Keri Warr
cfeb50826c
Add endpoint for reading currently authenticated user (#710) 2022-08-02 00:06:23 -07:00
Marshall Polaris
6901507461
Allow unspecfied outcome as input to sellshares (#706)
* Allow unspecfied outcome as input to `sellshares`

* Fix small details
2022-08-01 23:53:12 -07:00
Ian Philips
0b06ded5e5
Groups contracts (#709)
* Update group links in trigger and api

* Remove extra call during creation

* Remove grouplinks on frontend

* Deserialize

* Consolidate logic

* Move function locally
2022-08-01 21:15:09 -06:00
Marshall Polaris
b4e8c5d602
Backfill missing group IDs (#708) 2022-08-01 16:40:04 -06:00
Marshall Polaris
ec84245dd4
Add some API endpoints for reading group info (#707) 2022-08-01 14:59:45 -07:00
Ian Philips
0819c3918f Most popular=> Trending 2022-08-01 09:03:46 -06:00
ingawei
ae2e7dfe30
noobify welcome demo (#699)
* noobify welcome
* also beginning to add greyscale color scheme
2022-07-30 02:50:03 -05:00
James Grugett
87f6949d80 Use custom search index for search results. Hide sort options while there's a query. 2022-07-29 18:13:53 -07:00
James Grugett
003301762c Ignore filter on contract status when searching 2022-07-29 17:37:53 -07:00
James Grugett
d6cf4332da Delete query param when empty 2022-07-29 17:37:34 -07:00
James Grugett
be01a15230
Refactor search to not use Algolia components (#705)
* In progress refactor to not use Algolia components

* Cleanup

* Only query when necessary

* Read and update url params for query and sort

* Don't push router

* Don't update url if using default sort

* Add popstate listener to improve home navigation

* Remove console.logs
2022-07-29 19:08:51 -05:00
mantikoros
079a2a3936 eslint 2022-07-29 16:06:22 -07:00
mantikoros
5812d8ed2e manalink qr code 2022-07-29 16:02:18 -07:00
mantikoros
779b6dfc0c manalink referrals 2022-07-29 15:09:48 -07:00
mantikoros
bdea739c55 multiple choice contract card 2022-07-29 09:20:49 -07:00
Austin Chen
693eb96d23 Include groupId when duplicating markets 2022-07-29 09:20:01 -07:00
mantikoros
ada3f0787c create: add bottom margin 2022-07-28 11:44:07 -07:00
Ian Philips
d3da6de5dd Groups default open 2022-07-28 11:37:26 -07:00
marsteralex
aa6d0d1750
add beasts (#700)
* fix https

* add beasts

* Remove extra file

* Prettier-ify code

* Prettier-ify

Co-authored-by: Austin Chen <akrolsmir@gmail.com>
2022-07-28 11:31:58 -07:00
Ian Philips
05b0ca5cdb I want to see others' referrals 2022-07-28 11:16:48 -07:00
mantikoros
b6a70641a0 fix modal 2022-07-27 19:51:34 -07:00
mantikoros
1aaae93113
Multiple choice markets (#698)
* multipe choice answers

* create multiple choice cloud function

* multi choice market page

* show outcome '0'

* stats: multi choice type

* update place bet

* answer doc id = outcome

* update resolve market

* prettier

* fix

* fix resolution
2022-07-27 21:40:33 -05:00
James Grugett
b1c4f018f9 Expose cancel bet api 2022-07-27 17:38:25 -07:00
Ian Philips
013ff1d941 Show api error on create contract 2022-07-26 16:44:51 -07:00
Ian Philips
f32e995baa Show referrals banner on user-page 2022-07-26 15:24:16 -07:00
Marshall Polaris
b506e96548
Implement "sell all shares" functionality in sellshares and expose API (#696)
* Change `sellshares` to be able to sell all shares

* Sell all shares properly on bet panel UI

* Add API route for selling shares, document
2022-07-26 12:47:19 -07:00
Marshall Polaris
ad46a60c4f
Clean up rendering of user bets list (#694)
* Clean up crufty markup in bets list

* Don't render bet tables in bets list until expanded

* Don't look up unfilled bets for every sell button
2022-07-26 00:10:22 -07:00
Marshall Polaris
7e4f4b9a87
Clean up a bunch of crufty stuff on user comments list (#693) 2022-07-26 00:10:11 -07:00
SirSaltyy
0c2bcceae2
Update charity.ts (#695)
Added Founder's Pledge Global Health and Development Fund.

Made new logos for all the Founder's Pledge charities to help distinguish between them.
2022-07-26 11:10:22 +09:00
Ian Philips
af25a6c795 Allow adding multiple contracts to group in modal 2022-07-25 18:27:43 -07:00
mantikoros
ec0e25e5ed create user: remove ip check 2022-07-25 18:25:23 -07:00
mantikoros
3107c8fe30 large bet warning 2022-07-25 18:11:29 -07:00
mantikoros
24124ac86a show sign up button on mobile on market page 2022-07-25 17:45:42 -07:00
Marshall Polaris
06948bb98b
Make setNotificationsAsSeen return a promise (#692) 2022-07-25 16:37:23 -07:00
Marshall Polaris
64462d6ab4
Make tabs components better (#691)
* Make better tabs components, apply to user page

* Remove fishy unused href property from tabs

* Remove tab ID property

* Clean up crufty markup in tabs component

* Fix naming to be right (thanks James!)
2022-07-25 13:27:09 -07:00
TrueMilli
e4f8c14fab
Image compression (#689)
* added image compression

* removed TODO
2022-07-25 12:51:51 -07:00
mantikoros
d8f96876a0 PlayMoneyDisclaimer copy; hide order book for signed out users 2022-07-25 12:29:29 -07:00
mantikoros
d82c7d7f3e “added liquidity” ⇒ “added a subsidy” 2022-07-25 12:22:38 -07:00
mantikoros
d982d0332c play money wording 2022-07-24 23:38:57 -07:00
mantikoros
df91310d0f PlayMoneyDisclaimer; hide limit orders for signed out users; infobox styling 2022-07-24 23:28:05 -07:00
mantikoros
e389f4cc3b referrals text 2022-07-24 22:50:33 -07:00
Marshall Polaris
9840742927 Fix overaggressive emulator running in dev.sh 2022-07-24 02:30:28 -07:00
Marshall Polaris
312b244e2a
Small backend cleanups (#643)
* Reuse DAY_MS in update-metrics job

* More concise transaction in cancelbet

* Remove some meaningless awaits

* Do less work in onCreateLiquidityProvision

* Do less work in onCreateAnswer
2022-07-24 00:45:45 -07:00
Marshall Polaris
a1d5d161dd
Revamp backend code to support good local function development (#657)
* Move concurrently dep upwards

* Add express as explicit dependency

* Accept just one HTTP method per endpoint

* Fix endpoint option coalescing

* Expressification of cloud functions

* Nicer logging of API requests

* Refactor web package.json

* Add ts-node and nodemon to dev dependencies, bring back cors

* Add scaffolding to point dev server at local functions

* Enable emulator in dev server scaffolding

* Fix up a little stuff I broke
2022-07-24 00:26:38 -07:00
Austin Chen
6ad43b02c7 Show the number of comments and bets 2022-07-24 00:11:35 -07:00
Olivia Appleton
1f655acddb
Add my market manager tool (#690) 2022-07-23 23:33:19 -07:00
Sinclair Chen
6c89e5f18f
Add @ mentions to editor (#670)
* Add @ mentions to editor

* Fix mention list not loading

* Sort mention list by prefix, follow count

* Render at mention with Linkify component

- mentions are now Next <Link> rather than <a>
- fix bug where editor.getText() returns [object Object] for mentions
- fix mention rendering for posted markets
2022-07-23 20:37:34 -07:00
Ian Philips
f4e4582913 Add group slug during create 2022-07-23 15:04:11 -06:00
Marshall Polaris
6c8c068327
Write script to fix old comments without IDs and user IDs (#680) 2022-07-23 13:48:28 -07:00
James Grugett
64f2dbbe71 Fix unused var 2022-07-23 15:26:08 -05:00
James Grugett
f43df42449 Change card to show volume instead of pool 2022-07-23 15:23:47 -05:00
James Grugett
71b20eb61a Tweak visually hidden style 2022-07-23 15:10:54 -05:00
James Grugett
7f42796724 Update algolia filters to use groupLinks.slug isntead of deprecated groupSlugs field. 2022-07-23 15:02:08 -05:00
Austin Chen
71880dfc98
Add a toolbar for images and iframes (#688)
* Add a toolbar for images and iframes

* Insert embed code via modal
2022-07-23 09:19:49 -07:00
James Grugett
408027dd6a Fix bug 2022-07-22 22:44:21 -05:00
James Grugett
2116b86aec Fix infinite loop in numeric limit bet 2022-07-22 21:03:08 -05:00
Ian Philips
56a579ff91 Don't filter for group contract ids 2022-07-22 16:44:03 -06:00
Ian Philips
abde013ab6 Re-get contracts to get updated links 2022-07-22 16:40:37 -06:00
Ian Philips
5f074206de
Backfill and forward fill contracts with group info (#686)
* Backfill and forward fill contracts with group info

* No nested queries :(

* Fix filter

* Pass empty arrays instead of undefined
2022-07-22 16:28:53 -06:00
James Grugett
5899c1f3c0 Fix lints 2022-07-22 16:30:07 -05:00
James Grugett
135160dd92 Remove custom placeholders. Just show '0' for limit inputs 2022-07-22 16:18:36 -05:00
James Grugett
a1d51e3778 Update labels for numeric market outcomes 2022-07-22 16:07:59 -05:00
James Grugett
f800570845 Improve range limit order UI 2022-07-22 16:03:55 -05:00
Ian Philips
d319b654ce Add creator id to unique bettor ids 2022-07-22 14:15:42 -06:00
Ian Philips
63d8e6739b Add title, mobile flex 2022-07-22 13:53:19 -06:00
James Grugett
d3d472f5d2 Hide "Your bets" when signed out. "For you" becomes "Featured" when signed out. 2022-07-22 14:50:29 -05:00
Ian Philips
6fb9849007
Allow to add/remove from groups on market page (#685)
* Allow to add/remove from groups on market page

* remove lib

* Fix Sinclair's relative import from May

* Clean
2022-07-22 11:34:10 -06:00
mantikoros
163c990e9d "bettors" => "traders" 2022-07-22 12:03:33 -05:00
mantikoros
c3a0326b1e homepage seo 2022-07-22 12:01:52 -05:00
mantikoros
e13f4d3d4d charity description 2022-07-22 11:59:25 -05:00
mantikoros
2c80133856 add SEO tags to everything 2022-07-22 11:56:03 -05:00
mantikoros
de53a13c84 fix referrals seo 2022-07-22 11:25:48 -05:00
mantikoros
624df76393 search: sort by liquidity; remove oldest 2022-07-22 11:24:25 -05:00
Austin Chen
7cace82b83
Render iframes inside the rich text editor (#682)
* Try embedding iframes in tiptap

* When iframe code is pasted, inject it into the editor

* Code cleanups and comments

* Remove clsx dependency

Cuz it doesn't exist in `common` anyways

* Rename to tiptap-iframe
2022-07-22 09:12:23 -07:00
Austin Chen
87170894e2 Suppress eslint warning for script 2022-07-22 09:12:01 -07:00
Ian Philips
83cb0a6130 Allow clickable username in welcome message 2022-07-22 08:19:06 -06:00
Ian Philips
bfb11339ca Convert world and culture categories 2022-07-22 08:12:40 -06:00
Marshall Polaris
08fd27cb26
Make main login/logout buttons reload server side props (#677)
* Set cookies in auth handler before looking up user

* Make sidebar logout button trigger SSR reload

* Make sidebar login button trigger SSR reload
2022-07-22 00:03:16 -07:00
James Grugett
3b953a7c21
Range limit orders (#655)
* Prototype range limit order UI

* Conditionally show YES or NO max payout

* Range bet executes both bets immediately.

* Validate lowLimitProb < highLimitProb

* Show error if low limit is higher than high limit

* Update range order UI

* Revert "Validate lowLimitProb < highLimitProb"

This reverts commit c261fc2743.

* Revert "Range bet executes both bets immediately."

This reverts commit 30b95d75d9.

* Buy panel only non-limit orders

* Bet choice => outcome

* More iterating on range UI

* betChoice => outcome

* Lighten placeholder text
2022-07-22 00:57:56 -05:00
James Grugett
23b704ffe0 Fix excessive bottom margin on chart 2022-07-21 21:51:20 -05:00
James Grugett
ca5ca9b2b8 Refactor: Move ContractLeaderboard to its own file 2022-07-21 21:39:06 -05:00
ingawei
7474c0a0fd
Inga/manalinks pagination bug (#678)
* manalink pagination fix
* also fixed new manalink timing out bug
2022-07-21 20:22:17 -05:00
Sinclair Chen
4b4734531f
refactor createNotif - put ?: args in object (#681) 2022-07-21 17:08:09 -07:00
mantikoros
cded3f50ff "question" => "market" (controversial!) 2022-07-21 18:17:02 -05:00
mantikoros
80b27fdf6e Put leadeboards back in sidebar 2022-07-21 17:23:55 -05:00
Ian Philips
daca6ef482 Bonus=>10 2022-07-21 15:33:21 -06:00
mantikoros
91bec9c996
Referrals page (#676)
* Referrals page added to sidebar; useSaveReferral; InfoBox

* text color

* eslint

* migrate useUsers hook to react-query (#674)

* Remove bet button from free response comments

* Make answer replies more closely spaced together

* Host Ida and Alex's MTG Guesser game (#656)

* Copy over code from Mtg Guesser

* Run Prettier

* CSS Tweaks: Hover feedback, button positioning

* Hide all but counterspell & burn, for now

* Move to /mtg directory

* Fix prettierignore

* smaller jsons (#673)

limited burn to only red cards and also added limited json files to only have fields needed to play

* Add Ida's tweak to card position

Co-authored-by: marsteralex <bob.masteralex@gmail.com>

* Adjust card positioning

* Make the select screen index.html

* Remove other guessing games

* Remove alternate versions; add Alex's email

* Remove unused jsons

* Small FR comments tweaks

* Spacing tweak

* Changing manalinks table UI (#665)

From table to card view

* Fix comment spacing on non-FR

* Move "Send M$" lower in sidebar More list.

* Move leaderboards up in mobile nav

* eslint

* prettier

Co-authored-by: Sinclair Chen <abc.sinclair@gmail.com>
Co-authored-by: James Grugett <jahooma@gmail.com>
Co-authored-by: Austin Chen <akrolsmir@gmail.com>
Co-authored-by: marsteralex <bob.masteralex@gmail.com>
Co-authored-by: ingawei <46611122+ingawei@users.noreply.github.com>
2022-07-21 14:43:10 -05:00
mantikoros
96e9f749d2 track search categories 2022-07-21 12:45:47 -05:00
Austin Chen
6603effd1b
Use https for hotlinked image
Editing main from my phone, fingers crossed
2022-07-21 01:16:21 -07:00
Marshall Polaris
03858e4a8c
Make a React context to be the source of truth for authenticated user (#675)
* Make a React context to manage the logged in user events

* Remove unnecessary new user creation promise machinery

* Slight refactoring to auth context code

* Improvements in response to James feedback
2022-07-21 00:38:26 -07:00
James Grugett
8aa360c853 Move leaderboards up in mobile nav 2022-07-21 00:52:11 -05:00
James Grugett
21c08aed30 Move "Send M$" lower in sidebar More list. 2022-07-21 00:50:28 -05:00
James Grugett
2ad7266283 Fix comment spacing on non-FR 2022-07-21 00:46:57 -05:00
ingawei
7a041fd753
Changing manalinks table UI (#665)
From table to card view
2022-07-21 00:45:53 -05:00
James Grugett
f7151f131d Spacing tweak 2022-07-20 22:37:43 -05:00
James Grugett
8f5e51a304 Small FR comments tweaks 2022-07-20 22:13:37 -05:00
Austin Chen
aba818a9de Remove unused jsons 2022-07-20 18:05:41 -07:00
Austin Chen
260f4641dd Remove alternate versions; add Alex's email 2022-07-20 18:04:54 -07:00
Austin Chen
edee910e2d Remove other guessing games 2022-07-20 18:00:18 -07:00
Austin Chen
6b5b9b42f5 Make the select screen index.html 2022-07-20 17:14:49 -07:00
Austin Chen
c3b825cc44 Adjust card positioning 2022-07-20 16:59:40 -07:00
Austin Chen
a3f150b1d9
Host Ida and Alex's MTG Guesser game (#656)
* Copy over code from Mtg Guesser

* Run Prettier

* CSS Tweaks: Hover feedback, button positioning

* Hide all but counterspell & burn, for now

* Move to /mtg directory

* Fix prettierignore

* smaller jsons (#673)

limited burn to only red cards and also added limited json files to only have fields needed to play

* Add Ida's tweak to card position

Co-authored-by: marsteralex <bob.masteralex@gmail.com>
2022-07-20 16:57:51 -07:00
James Grugett
528dd2b28a Make answer replies more closely spaced together 2022-07-20 18:35:09 -05:00
James Grugett
5ddf496dae Remove bet button from free response comments 2022-07-20 18:35:09 -05:00
Sinclair Chen
aa554ca9f6
migrate useUsers hook to react-query (#674) 2022-07-20 16:31:18 -07:00
Marshall Polaris
ace39ef73d
Update Next.js 12.1.2 -> 12.2.0 (#669)
* Update Next.js 12.1.2 -> 12.2.0

* Further bump Next to 12.2.2
2022-07-20 15:42:31 -07:00
mantikoros
49dcd97d70 feed bets: better prob display 2022-07-20 17:04:11 -05:00
mantikoros
75a1d606cb feed bets: show change in prob 2022-07-20 16:28:25 -05:00
mantikoros
302a635542 group page max width 2022-07-20 16:06:31 -05:00
James Grugett
c35d0a8bc6 Split out "Your bets" from "For you" 2022-07-20 15:30:07 -05:00
James Grugett
44afa92b58 Turn off for you by default 2022-07-20 15:05:48 -05:00
James Grugett
e45d81513c Don't filter by personal unless pills enabled 2022-07-20 14:49:16 -05:00
Sinclair Chen
0870397fea Show line in menu on mobile 2022-07-20 12:36:23 -07:00
Sinclair Chen
202132868f lint and prettier 2022-07-20 12:35:04 -07:00
mantikoros
d65a60984d make group invite link more prominent 2022-07-20 11:45:53 -05:00
mantikoros
45b883477d generic copy link button 2022-07-20 11:42:49 -05:00
mantikoros
b60892fada group 'rankings' => 'leaderboards' (friendlier, more consistent terminology) 2022-07-20 11:15:55 -05:00
Marshall Polaris
c8361f1748
Make it so that if you sign in on / you get redirected to /home (#672) 2022-07-20 01:59:14 -07:00
mantikoros
b517f7cfa7 eslint; remove unused tags import 2022-07-20 00:35:27 -05:00
mantikoros
2b13085dff search: use default categories for non-authed users 2022-07-20 00:23:00 -05:00
mantikoros
0013f76873 search defaults to 'for you'; hide pills for additional filters 2022-07-20 00:03:03 -05:00
mantikoros
83e9408d69 remove tags from info panel 2022-07-19 23:48:09 -05:00
Marshall Polaris
bacd546e5d Fix unused import from Ian's code 2022-07-19 20:10:54 -07:00
Marshall Polaris
61094ea17d
Properly handle expired ID token cookie, be robust to errors (#671) 2022-07-19 20:08:33 -07:00
James Grugett
b2c89d36cf Home: Show pills that are groups (in addition to All, For you) 2022-07-19 18:23:52 -05:00
Ian Philips
921ac4b2a9 Fix last 3 days number 2022-07-19 17:22:23 -06:00
mantikoros
b48e910f70 fix areaBaselineValue 2022-07-19 18:20:03 -05:00
mantikoros
bab828412b group: add question button 2022-07-19 18:16:03 -05:00
Ian Philips
1f0983a145 Find old contracts to decrement score on 2022-07-19 17:08:51 -06:00
Ian Philips
4aface583d Remove pesky loading spinner 2022-07-19 16:41:11 -06:00
Ian Philips
2152e5286a Score & sort by unique bettors in last 3 days 2022-07-19 16:29:41 -06:00
James Grugett
58d6286361 Fix chart area extending into labels below 2022-07-19 17:22:58 -05:00
James Grugett
6124ea01f6 Fix a DOM error in console 2022-07-19 16:57:32 -05:00
Marshall Polaris
6d3490cd68
Turn off Next.js font inlining (#668) 2022-07-19 14:20:23 -07:00
James Grugett
af6552958f Show all-time profit on UserPage 2022-07-19 16:05:50 -05:00
Marshall Polaris
f9aab39039
Clean up font loading and see if it fixes our problem (#667) 2022-07-19 13:57:32 -07:00
James Grugett
fc9e261601 Fix build 2022-07-19 15:45:47 -05:00
James Grugett
e9ad30cc74 Increase number of contracts shown in bets list 2022-07-19 15:43:37 -05:00
James Grugett
2684c8bcca Default to weekly leaderboard 2022-07-19 15:39:40 -05:00
James Grugett
6c070464dd Use static props to load leaderboard fast 2022-07-19 15:39:17 -05:00
Ian Philips
b5ef7490c3 NotificationSettings to its own file 2022-07-19 14:24:36 -06:00
571 changed files with 45847 additions and 17971 deletions

43
.github/workflows/format.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Reformat main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
env:
FORCE_COLOR: 3
NEXT_TELEMETRY_DISABLED: 1
# mqp - i generated a personal token to use for these writes -- it's unclear
# why, but the default token didn't work, even when i gave it max permissions
jobs:
prettify:
name: Auto-prettify
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
- name: Restore cached node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install missing dependencies
run: yarn install --prefer-offline --frozen-lockfile
- name: Run Prettier on web client
working-directory: web
run: yarn format
- name: Commit any Prettier changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Auto-prettification
branch: ${{ github.head_ref }}

43
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Run linter (remove unused imports)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
env:
FORCE_COLOR: 3
NEXT_TELEMETRY_DISABLED: 1
# mqp - i generated a personal token to use for these writes -- it's unclear
# why, but the default token didn't work, even when i gave it max permissions
jobs:
lint:
name: Auto-lint
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
token: ${{ secrets.FORMATTER_ACCESS_TOKEN }}
- name: Restore cached node_modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
- name: Install missing dependencies
run: yarn install --prefer-offline --frozen-lockfile
- name: Run lint script
run: yarn lint
- name: Commit any lint changes
if: always()
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: Auto-remove unused imports
branch: ${{ github.head_ref }}

View File

@ -0,0 +1,17 @@
name: Merge main into main2 on every commit
on:
push:
branches:
- 'main'
jobs:
merge-branch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Merge main -> main2
uses: devmasx/merge-branch@master
with:
type: now
target_branch: main2
github_token: ${{ github.token }}

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
plugins: ['lodash'], plugins: ['lodash', 'unused-imports'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
ignorePatterns: ['lib'], ignorePatterns: ['lib'],
env: { env: {
@ -26,6 +26,7 @@ module.exports = {
caughtErrorsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_',
}, },
], ],
'unused-imports/no-unused-imports': 'warn',
}, },
}, },
], ],

View File

@ -1,33 +1,30 @@
import { addCpmmLiquidity, getCpmmLiquidity } from './calculate-cpmm' import { getCpmmLiquidity } from './calculate-cpmm'
import { CPMMContract } from './contract' import { CPMMContract } from './contract'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { User } from './user'
export const getNewLiquidityProvision = ( export const getNewLiquidityProvision = (
user: User, userId: string,
amount: number, amount: number,
contract: CPMMContract, contract: CPMMContract,
newLiquidityProvisionId: string newLiquidityProvisionId: string
) => { ) => {
const { pool, p, totalLiquidity } = contract const { pool, p, totalLiquidity, subsidyPool } = contract
const { newPool, newP } = addCpmmLiquidity(pool, p, amount) const liquidity = getCpmmLiquidity(pool, p)
const liquidity =
getCpmmLiquidity(newPool, newP) - getCpmmLiquidity(pool, newP)
const newLiquidityProvision: LiquidityProvision = { const newLiquidityProvision: LiquidityProvision = {
id: newLiquidityProvisionId, id: newLiquidityProvisionId,
userId: user.id, userId: userId,
contractId: contract.id, contractId: contract.id,
amount, amount,
pool: newPool, pool,
p: newP, p,
liquidity, liquidity,
createdTime: Date.now(), createdTime: Date.now(),
} }
const newTotalLiquidity = (totalLiquidity ?? 0) + amount const newTotalLiquidity = (totalLiquidity ?? 0) + amount
const newSubsidyPool = (subsidyPool ?? 0) + amount
return { newLiquidityProvision, newPool, newP, newTotalLiquidity } return { newLiquidityProvision, newTotalLiquidity, newSubsidyPool }
} }

View File

@ -5,17 +5,22 @@ import {
CPMMBinaryContract, CPMMBinaryContract,
DPMBinaryContract, DPMBinaryContract,
FreeResponseContract, FreeResponseContract,
MultipleChoiceContract,
NumericContract, NumericContract,
} from './contract' } from './contract'
import { User } from './user' import { User } from './user'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { noFees } from './fees' import { noFees } from './fees'
import { ENV_CONFIG } from './envs/constants' import { Answer } from './answer'
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id
export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id
export const UNIQUE_BETTOR_LIQUIDITY_AMOUNT = 20
type NormalizedBet<T extends Bet = Bet> = Omit<
T,
'userAvatarUrl' | 'userName' | 'userUsername'
>
export function getCpmmInitialLiquidity( export function getCpmmInitialLiquidity(
providerId: string, providerId: string,
@ -52,7 +57,7 @@ export function getAnteBets(
const { createdTime } = contract const { createdTime } = contract
const yesBet: Bet = { const yesBet: NormalizedBet = {
id: yesAnteId, id: yesAnteId,
userId: creator.id, userId: creator.id,
contractId: contract.id, contractId: contract.id,
@ -66,7 +71,7 @@ export function getAnteBets(
fees: noFees, fees: noFees,
} }
const noBet: Bet = { const noBet: NormalizedBet = {
id: noAnteId, id: noAnteId,
userId: creator.id, userId: creator.id,
contractId: contract.id, contractId: contract.id,
@ -94,7 +99,7 @@ export function getFreeAnswerAnte(
const { createdTime } = contract const { createdTime } = contract
const anteBet: Bet = { const anteBet: NormalizedBet = {
id: anteBetId, id: anteBetId,
userId: anteBettorId, userId: anteBettorId,
contractId: contract.id, contractId: contract.id,
@ -111,6 +116,50 @@ export function getFreeAnswerAnte(
return anteBet return anteBet
} }
export function getMultipleChoiceAntes(
creator: User,
contract: MultipleChoiceContract,
answers: string[],
betDocIds: string[]
) {
const { totalBets, totalShares } = contract
const amount = totalBets['0']
const shares = totalShares['0']
const p = 1 / answers.length
const { createdTime } = contract
const bets: NormalizedBet[] = answers.map((answer, i) => ({
id: betDocIds[i],
userId: creator.id,
contractId: contract.id,
amount,
shares,
outcome: i.toString(),
probBefore: p,
probAfter: p,
createdTime,
isAnte: true,
fees: noFees,
}))
const { username, name, avatarUrl } = creator
const answerObjects: Answer[] = answers.map((answer, i) => ({
id: i.toString(),
number: i,
contractId: contract.id,
createdTime,
userId: creator.id,
username,
name,
avatarUrl,
text: answer,
}))
return { bets, answerObjects }
}
export function getNumericAnte( export function getNumericAnte(
anteBettorId: string, anteBettorId: string,
contract: NumericContract, contract: NumericContract,
@ -130,7 +179,7 @@ export function getNumericAnte(
range(0, bucketCount).map((_, i) => [i, betAnte]) range(0, bucketCount).map((_, i) => [i, betAnte])
) )
const anteBet: NumericBet = { const anteBet: NormalizedBet<NumericBet> = {
id: newBetId, id: newBetId,
userId: anteBettorId, userId: anteBettorId,
contractId: contract.id, contractId: contract.id,

View File

@ -12,7 +12,9 @@ export class APIError extends Error {
} }
export function getFunctionUrl(name: string) { export function getFunctionUrl(name: string) {
if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) { if (process.env.NEXT_PUBLIC_FUNCTIONS_URL) {
return `${process.env.NEXT_PUBLIC_FUNCTIONS_URL}/${name}`
} else if (process.env.NEXT_PUBLIC_FIREBASE_EMULATE) {
const { projectId, region } = ENV_CONFIG.firebaseConfig const { projectId, region } = ENV_CONFIG.firebaseConfig
return `http://localhost:5001/${projectId}/${region}/${name}` return `http://localhost:5001/${projectId}/${region}/${name}`
} else { } else {

123
common/badge.ts Normal file
View File

@ -0,0 +1,123 @@
import { User } from './user'
export type Badge = {
type: BadgeTypes
createdTime: number
data: { [key: string]: any }
name: 'Proven Correct' | 'Streaker' | 'Market Creator'
}
export type BadgeTypes = 'PROVEN_CORRECT' | 'STREAKER' | 'MARKET_CREATOR'
export type ProvenCorrectBadgeData = {
type: 'PROVEN_CORRECT'
data: {
contractSlug: string
contractCreatorUsername: string
contractTitle: string
commentId: string
betAmount: number
}
}
export type MarketCreatorBadgeData = {
type: 'MARKET_CREATOR'
data: {
totalContractsCreated: number
}
}
export type StreakerBadgeData = {
type: 'STREAKER'
data: {
totalBettingStreak: number
}
}
export type ProvenCorrectBadge = Badge & ProvenCorrectBadgeData
export type StreakerBadge = Badge & StreakerBadgeData
export type MarketCreatorBadge = Badge & MarketCreatorBadgeData
export const MINIMUM_UNIQUE_BETTORS_FOR_PROVEN_CORRECT_BADGE = 5
export const provenCorrectRarityThresholds = [1, 1000, 10000]
const calculateProvenCorrectBadgeRarity = (badge: ProvenCorrectBadge) => {
const { betAmount } = badge.data
const thresholdArray = provenCorrectRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (betAmount >= thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export const streakerBadgeRarityThresholds = [1, 50, 250]
const calculateStreakerBadgeRarity = (badge: StreakerBadge) => {
const { totalBettingStreak } = badge.data
const thresholdArray = streakerBadgeRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (totalBettingStreak == thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export const marketCreatorBadgeRarityThresholds = [1, 75, 300]
const calculateMarketCreatorBadgeRarity = (badge: MarketCreatorBadge) => {
const { totalContractsCreated } = badge.data
const thresholdArray = marketCreatorBadgeRarityThresholds
let i = thresholdArray.length - 1
while (i >= 0) {
if (totalContractsCreated == thresholdArray[i]) {
return i + 1
}
i--
}
return 1
}
export type rarities = 'bronze' | 'silver' | 'gold'
const rarityRanks: { [key: number]: rarities } = {
1: 'bronze',
2: 'silver',
3: 'gold',
}
export const calculateBadgeRarity = (badge: Badge) => {
switch (badge.type) {
case 'PROVEN_CORRECT':
return rarityRanks[
calculateProvenCorrectBadgeRarity(badge as ProvenCorrectBadge)
]
case 'MARKET_CREATOR':
return rarityRanks[
calculateMarketCreatorBadgeRarity(badge as MarketCreatorBadge)
]
case 'STREAKER':
return rarityRanks[calculateStreakerBadgeRarity(badge as StreakerBadge)]
default:
return rarityRanks[0]
}
}
export const getBadgesByRarity = (user: User | null | undefined) => {
const rarities: { [key in rarities]: number } = {
bronze: 0,
silver: 0,
gold: 0,
}
if (!user) return rarities
Object.values(user.achievements).map((value) => {
value.badges.map((badge) => {
rarities[calculateBadgeRarity(badge)] =
(rarities[calculateBadgeRarity(badge)] ?? 0) + 1
})
})
return rarities
}

View File

@ -3,6 +3,12 @@ import { Fees } from './fees'
export type Bet = { export type Bet = {
id: string id: string
userId: string userId: string
// denormalized for bet lists
userAvatarUrl?: string
userUsername: string
userName: string
contractId: string contractId: string
createdTime: number createdTime: number
@ -14,18 +20,21 @@ export type Bet = {
probBefore: number probBefore: number
probAfter: number probAfter: number
sale?: {
amount: number // amount user makes from sale
betId: string // id of bet being sold
// TODO: add sale time?
}
fees: Fees fees: Fees
isSold?: boolean // true if this BUY bet has been sold
isAnte?: boolean isAnte?: boolean
isLiquidityProvision?: boolean isLiquidityProvision?: boolean
isRedemption?: boolean isRedemption?: boolean
challengeSlug?: string
// Props for bets in DPM contract below.
// A bet is either a BUY or a SELL that sells all of a previous buy.
isSold?: boolean // true if this BUY bet has been sold
// This field marks a SELL bet.
sale?: {
amount: number // amount user makes from sale
betId: string // id of BUY bet being sold
}
} & Partial<LimitProps> } & Partial<LimitProps>
export type NumericBet = Bet & { export type NumericBet = Bet & {
@ -58,5 +67,3 @@ export type fill = {
// I.e. -fill.shares === matchedBet.shares // I.e. -fill.shares === matchedBet.shares
isSale?: boolean isSale?: boolean
} }
export const MAX_LOAN_PER_CONTRACT = 20

View File

@ -1,11 +1,10 @@
import { sum, groupBy, mapValues, sumBy } from 'lodash' import { groupBy, mapValues, sumBy } from 'lodash'
import { LimitBet } from './bet' import { LimitBet } from './bet'
import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees' import { CREATOR_FEE, Fees, LIQUIDITY_FEE, PLATFORM_FEE } from './fees'
import { LiquidityProvision } from './liquidity-provision' import { LiquidityProvision } from './liquidity-provision'
import { computeFills } from './new-bet' import { computeFills } from './new-bet'
import { binarySearch } from './util/algos' import { binarySearch } from './util/algos'
import { addObjects } from './util/object'
export type CpmmState = { export type CpmmState = {
pool: { [outcome: string]: number } pool: { [outcome: string]: number }
@ -123,6 +122,7 @@ export function calculateCpmmAmountToProb(
prob: number, prob: number,
outcome: 'YES' | 'NO' outcome: 'YES' | 'NO'
) { ) {
if (prob <= 0 || prob >= 1 || isNaN(prob)) return Infinity
if (outcome === 'NO') prob = 1 - prob if (outcome === 'NO') prob = 1 - prob
// First, find an upper bound that leads to a more extreme probability than prob. // First, find an upper bound that leads to a more extreme probability than prob.
@ -146,7 +146,8 @@ function calculateAmountToBuyShares(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
// Search for amount between bounds (0, shares). // Search for amount between bounds (0, shares).
// Min share price is M$0, and max is M$1 each. // Min share price is M$0, and max is M$1 each.
@ -156,7 +157,8 @@ function calculateAmountToBuyShares(
amount, amount,
state, state,
undefined, undefined,
unfilledBets unfilledBets,
balanceByUserId
) )
const totalShares = sumBy(takers, (taker) => taker.shares) const totalShares = sumBy(takers, (taker) => taker.shares)
@ -168,7 +170,8 @@ export function calculateCpmmSale(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
if (Math.round(shares) < 0) { if (Math.round(shares) < 0) {
throw new Error('Cannot sell non-positive shares') throw new Error('Cannot sell non-positive shares')
@ -179,15 +182,17 @@ export function calculateCpmmSale(
state, state,
shares, shares,
oppositeOutcome, oppositeOutcome,
unfilledBets unfilledBets,
balanceByUserId
) )
const { cpmmState, makers, takers, totalFees } = computeFills( const { cpmmState, makers, takers, totalFees, ordersToCancel } = computeFills(
oppositeOutcome, oppositeOutcome,
buyAmount, buyAmount,
state, state,
undefined, undefined,
unfilledBets unfilledBets,
balanceByUserId
) )
// Transform buys of opposite outcome into sells. // Transform buys of opposite outcome into sells.
@ -210,6 +215,7 @@ export function calculateCpmmSale(
fees: totalFees, fees: totalFees,
makers, makers,
takers: saleTakers, takers: saleTakers,
ordersToCancel,
} }
} }
@ -217,9 +223,16 @@ export function getCpmmProbabilityAfterSale(
state: CpmmState, state: CpmmState,
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
const { cpmmState } = calculateCpmmSale(state, shares, outcome, unfilledBets) const { cpmmState } = calculateCpmmSale(
state,
shares,
outcome,
unfilledBets,
balanceByUserId
)
return getCpmmProbability(cpmmState.pool, cpmmState.p) return getCpmmProbability(cpmmState.pool, cpmmState.p)
} }
@ -253,48 +266,22 @@ export function addCpmmLiquidity(
return { newPool, liquidity, newP } return { newPool, liquidity, newP }
} }
const calculateLiquidityDelta = (p: number) => (l: LiquidityProvision) => { export function getCpmmLiquidityPoolWeights(liquidities: LiquidityProvision[]) {
const oldLiquidity = getCpmmLiquidity(l.pool, p) const userAmounts = groupBy(liquidities, (w) => w.userId)
const totalAmount = sumBy(liquidities, (w) => w.amount)
const newPool = addObjects(l.pool, { YES: l.amount, NO: l.amount }) return mapValues(
const newLiquidity = getCpmmLiquidity(newPool, p) userAmounts,
(amounts) => sumBy(amounts, (w) => w.amount) / totalAmount
const liquidity = newLiquidity - oldLiquidity
return liquidity
}
export function getCpmmLiquidityPoolWeights(
state: CpmmState,
liquidities: LiquidityProvision[],
excludeAntes: boolean
) {
const calcLiqudity = calculateLiquidityDelta(state.p)
const liquidityShares = liquidities.map(calcLiqudity)
const shareSum = sum(liquidityShares)
const weights = liquidityShares.map((shares, i) => ({
weight: shares / shareSum,
providerId: liquidities[i].userId,
}))
const includedWeights = excludeAntes
? weights.filter((_, i) => !liquidities[i].isAnte)
: weights
const userWeights = groupBy(includedWeights, (w) => w.providerId)
const totalUserWeights = mapValues(userWeights, (userWeight) =>
sumBy(userWeight, (w) => w.weight)
) )
return totalUserWeights
} }
export function getUserLiquidityShares( export function getUserLiquidityShares(
userId: string, userId: string,
state: CpmmState, state: CpmmState,
liquidities: LiquidityProvision[], liquidities: LiquidityProvision[]
excludeAntes: boolean
) { ) {
const weights = getCpmmLiquidityPoolWeights(state, liquidities, excludeAntes) const weights = getCpmmLiquidityPoolWeights(liquidities)
const userWeight = weights[userId] ?? 0 const userWeight = weights[userId] ?? 0
return mapValues(state.pool, (shares) => userWeight * shares) return mapValues(state.pool, (shares) => userWeight * shares)

315
common/calculate-metrics.ts Normal file
View File

@ -0,0 +1,315 @@
import { Dictionary, groupBy, last, partition, sum, sumBy, uniq } from 'lodash'
import { calculatePayout, getContractBetMetrics } from './calculate'
import { Bet, LimitBet } from './bet'
import {
Contract,
CPMMBinaryContract,
CPMMContract,
DPMContract,
} from './contract'
import { PortfolioMetrics, User } from './user'
import { DAY_MS } from './util/time'
import { getBinaryCpmmBetInfo, getNewMultiBetInfo } from './new-bet'
import { getCpmmProbability } from './calculate-cpmm'
import { removeUndefinedProps } from './util/object'
const computeInvestmentValue = (
bets: Bet[],
contractsDict: { [k: string]: Contract }
) => {
return sumBy(bets, (bet) => {
const contract = contractsDict[bet.contractId]
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
const payout = calculatePayout(contract, bet, 'MKT')
const value = payout - (bet.loanAmount ?? 0)
if (isNaN(value)) return 0
return value
})
}
export const computeInvestmentValueCustomProb = (
bets: Bet[],
contract: Contract,
p: number
) => {
return sumBy(bets, (bet) => {
if (!contract || contract.isResolved) return 0
if (bet.sale || bet.isSold) return 0
const { outcome, shares } = bet
const betP = outcome === 'YES' ? p : 1 - p
const value = betP * shares
if (isNaN(value)) return 0
return value
})
}
export const computeElasticity = (
bets: Bet[],
contract: Contract,
betAmount = 50
) => {
const { mechanism, outcomeType } = contract
return mechanism === 'cpmm-1' &&
(outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC')
? computeBinaryCpmmElasticity(bets, contract, betAmount)
: computeDpmElasticity(contract, betAmount)
}
export const computeBinaryCpmmElasticity = (
bets: Bet[],
contract: CPMMContract,
betAmount: number
) => {
const limitBets = bets
.filter(
(b) =>
!b.isFilled &&
!b.isSold &&
!b.isRedemption &&
!b.sale &&
!b.isCancelled &&
b.limitProb !== undefined
)
.sort((a, b) => a.createdTime - b.createdTime) as LimitBet[]
const userIds = uniq(limitBets.map((b) => b.userId))
// Assume all limit orders are good.
const userBalances = Object.fromEntries(
userIds.map((id) => [id, Number.MAX_SAFE_INTEGER])
)
const { newPool: poolY, newP: pY } = getBinaryCpmmBetInfo(
'YES',
betAmount,
contract,
undefined,
limitBets,
userBalances
)
const resultYes = getCpmmProbability(poolY, pY)
const { newPool: poolN, newP: pN } = getBinaryCpmmBetInfo(
'NO',
betAmount,
contract,
undefined,
limitBets,
userBalances
)
const resultNo = getCpmmProbability(poolN, pN)
// handle AMM overflow
const safeYes = Number.isFinite(resultYes) ? resultYes : 1
const safeNo = Number.isFinite(resultNo) ? resultNo : 0
return safeYes - safeNo
}
export const computeDpmElasticity = (
contract: DPMContract,
betAmount: number
) => {
return getNewMultiBetInfo('', 2 * betAmount, contract).newBet.probAfter
}
const computeTotalPool = (userContracts: Contract[], startTime = 0) => {
const periodFilteredContracts = userContracts.filter(
(contract) => contract.createdTime >= startTime
)
return sum(
periodFilteredContracts.map((contract) => sum(Object.values(contract.pool)))
)
}
export const computeVolume = (contractBets: Bet[], since: number) => {
return sumBy(contractBets, (b) =>
b.createdTime > since && !b.isRedemption ? Math.abs(b.amount) : 0
)
}
const calculateProbChangeSince = (descendingBets: Bet[], since: number) => {
const newestBet = descendingBets[0]
if (!newestBet) return 0
const betBeforeSince = descendingBets.find((b) => b.createdTime < since)
if (!betBeforeSince) {
const oldestBet = last(descendingBets) ?? newestBet
return newestBet.probAfter - oldestBet.probBefore
}
return newestBet.probAfter - betBeforeSince.probAfter
}
export const calculateProbChanges = (descendingBets: Bet[]) => {
const now = Date.now()
const yesterday = now - DAY_MS
const weekAgo = now - 7 * DAY_MS
const monthAgo = now - 30 * DAY_MS
return {
day: calculateProbChangeSince(descendingBets, yesterday),
week: calculateProbChangeSince(descendingBets, weekAgo),
month: calculateProbChangeSince(descendingBets, monthAgo),
}
}
export const calculateCreatorVolume = (userContracts: Contract[]) => {
const allTimeCreatorVolume = computeTotalPool(userContracts, 0)
const monthlyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 30 * DAY_MS
)
const weeklyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 7 * DAY_MS
)
const dailyCreatorVolume = computeTotalPool(
userContracts,
Date.now() - 1 * DAY_MS
)
return {
daily: dailyCreatorVolume,
weekly: weeklyCreatorVolume,
monthly: monthlyCreatorVolume,
allTime: allTimeCreatorVolume,
}
}
export const calculateNewPortfolioMetrics = (
user: User,
contractsById: { [k: string]: Contract },
currentBets: Bet[]
) => {
const investmentValue = computeInvestmentValue(currentBets, contractsById)
const newPortfolio = {
investmentValue: investmentValue,
balance: user.balance,
totalDeposits: user.totalDeposits,
timestamp: Date.now(),
userId: user.id,
}
return newPortfolio
}
const calculateProfitForPeriod = (
startingPortfolio: PortfolioMetrics | undefined,
currentProfit: number
) => {
if (startingPortfolio === undefined) {
return currentProfit
}
const startingProfit = calculatePortfolioProfit(startingPortfolio)
return currentProfit - startingProfit
}
export const calculatePortfolioProfit = (portfolio: PortfolioMetrics) => {
return portfolio.investmentValue + portfolio.balance - portfolio.totalDeposits
}
export const calculateNewProfit = (
portfolioHistory: Record<
'current' | 'day' | 'week' | 'month',
PortfolioMetrics | undefined
>,
newPortfolio: PortfolioMetrics
) => {
const allTimeProfit = calculatePortfolioProfit(newPortfolio)
const newProfit = {
daily: calculateProfitForPeriod(portfolioHistory.day, allTimeProfit),
weekly: calculateProfitForPeriod(portfolioHistory.week, allTimeProfit),
monthly: calculateProfitForPeriod(portfolioHistory.month, allTimeProfit),
allTime: allTimeProfit,
}
return newProfit
}
export const calculateMetricsByContract = (
bets: Bet[],
contractsById: Dictionary<Contract>
) => {
const betsByContract = groupBy(bets, (bet) => bet.contractId)
const unresolvedContracts = Object.keys(betsByContract)
.map((cid) => contractsById[cid])
.filter((c) => c && !c.isResolved)
return unresolvedContracts.map((c) => {
const bets = betsByContract[c.id] ?? []
const current = getContractBetMetrics(c, bets)
let periodMetrics
if (c.mechanism === 'cpmm-1' && c.outcomeType === 'BINARY') {
const periods = ['day', 'week', 'month'] as const
periodMetrics = Object.fromEntries(
periods.map((period) => [
period,
calculatePeriodProfit(c, bets, period),
])
)
}
return removeUndefinedProps({
contractId: c.id,
...current,
from: periodMetrics,
})
})
}
export type ContractMetrics = ReturnType<
typeof calculateMetricsByContract
>[number]
const calculatePeriodProfit = (
contract: CPMMBinaryContract,
bets: Bet[],
period: 'day' | 'week' | 'month'
) => {
const days = period === 'day' ? 1 : period === 'week' ? 7 : 30
const fromTime = Date.now() - days * DAY_MS
const [previousBets, recentBets] = partition(
bets,
(b) => b.createdTime < fromTime
)
const prevProb = contract.prob - contract.probChanges[period]
const prob = contract.resolutionProbability
? contract.resolutionProbability
: contract.prob
const previousBetsValue = computeInvestmentValueCustomProb(
previousBets,
contract,
prevProb
)
const currentBetsValue = computeInvestmentValueCustomProb(
previousBets,
contract,
prob
)
const { profit: recentProfit, invested: recentInvested } =
getContractBetMetrics(contract, recentBets)
const profit = currentBetsValue - previousBetsValue + recentProfit
const invested = previousBetsValue + recentInvested
const profitPercent = invested === 0 ? 0 : 100 * (profit / invested)
return {
profit,
profitPercent,
invested,
prevValue: previousBetsValue,
value: currentBetsValue,
}
}

View File

@ -1,4 +1,4 @@
import { maxBy } from 'lodash' import { maxBy, partition, sortBy, sum, sumBy } from 'lodash'
import { Bet, LimitBet } from './bet' import { Bet, LimitBet } from './bet'
import { import {
calculateCpmmSale, calculateCpmmSale,
@ -23,6 +23,7 @@ import {
BinaryContract, BinaryContract,
FreeResponseContract, FreeResponseContract,
PseudoNumericContract, PseudoNumericContract,
MultipleChoiceContract,
} from './contract' } from './contract'
import { floatingEqual } from './util/math' import { floatingEqual } from './util/math'
@ -77,7 +78,8 @@ export function calculateShares(
export function calculateSaleAmount( export function calculateSaleAmount(
contract: Contract, contract: Contract,
bet: Bet, bet: Bet,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
return contract.mechanism === 'cpmm-1' && return contract.mechanism === 'cpmm-1' &&
(contract.outcomeType === 'BINARY' || (contract.outcomeType === 'BINARY' ||
@ -86,7 +88,8 @@ export function calculateSaleAmount(
contract, contract,
Math.abs(bet.shares), Math.abs(bet.shares),
bet.outcome as 'YES' | 'NO', bet.outcome as 'YES' | 'NO',
unfilledBets unfilledBets,
balanceByUserId
).saleValue ).saleValue
: calculateDpmSaleAmount(contract, bet) : calculateDpmSaleAmount(contract, bet)
} }
@ -101,14 +104,16 @@ export function getProbabilityAfterSale(
contract: Contract, contract: Contract,
outcome: string, outcome: string,
shares: number, shares: number,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) { ) {
return contract.mechanism === 'cpmm-1' return contract.mechanism === 'cpmm-1'
? getCpmmProbabilityAfterSale( ? getCpmmProbabilityAfterSale(
contract, contract,
shares, shares,
outcome as 'YES' | 'NO', outcome as 'YES' | 'NO',
unfilledBets unfilledBets,
balanceByUserId
) )
: getDpmProbabilityAfterSale(contract.totalShares, outcome, shares) : getDpmProbabilityAfterSale(contract.totalShares, outcome, shares)
} }
@ -132,10 +137,53 @@ export function resolvedPayout(contract: Contract, bet: Bet) {
: calculateDpmPayout(contract, bet, outcome) : calculateDpmPayout(contract, bet, outcome)
} }
function getCpmmInvested(yourBets: Bet[]) {
const totalShares: { [outcome: string]: number } = {}
const totalSpent: { [outcome: string]: number } = {}
const sortedBets = sortBy(yourBets, 'createdTime')
for (const bet of sortedBets) {
const { outcome, shares, amount } = bet
if (floatingEqual(shares, 0)) continue
const spent = totalSpent[outcome] ?? 0
const position = totalShares[outcome] ?? 0
if (amount > 0) {
totalShares[outcome] = position + shares
totalSpent[outcome] = spent + amount
} else if (amount < 0) {
const averagePrice = position === 0 ? 0 : spent / position
totalShares[outcome] = position + shares
totalSpent[outcome] = spent + averagePrice * shares
}
}
return sum([0, ...Object.values(totalSpent)])
}
function getDpmInvested(yourBets: Bet[]) {
const sortedBets = sortBy(yourBets, 'createdTime')
return sumBy(sortedBets, (bet) => {
const { amount, sale } = bet
if (sale) {
const originalBet = sortedBets.find((b) => b.id === sale.betId)
if (originalBet) return -originalBet.amount
return 0
}
return amount
})
}
export type ContractBetMetrics = ReturnType<typeof getContractBetMetrics>
export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) { export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
const { resolution } = contract const { resolution } = contract
const isCpmm = contract.mechanism === 'cpmm-1'
let currentInvested = 0
let totalInvested = 0 let totalInvested = 0
let payout = 0 let payout = 0
let loan = 0 let loan = 0
@ -161,7 +209,6 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
saleValue -= amount saleValue -= amount
} }
currentInvested += amount
loan += loanAmount ?? 0 loan += loanAmount ?? 0
payout += resolution payout += resolution
? calculatePayout(contract, bet, resolution) ? calculatePayout(contract, bet, resolution)
@ -169,18 +216,18 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
} }
} }
const netPayout = payout - loan
const profit = payout + saleValue + redeemed - totalInvested const profit = payout + saleValue + redeemed - totalInvested
const profitPercent = (profit / totalInvested) * 100 const profitPercent = totalInvested === 0 ? 0 : (profit / totalInvested) * 100
const invested = isCpmm ? getCpmmInvested(yourBets) : getDpmInvested(yourBets)
const hasShares = Object.values(totalShares).some( const hasShares = Object.values(totalShares).some(
(shares) => !floatingEqual(shares, 0) (shares) => !floatingEqual(shares, 0)
) )
return { return {
invested: Math.max(0, currentInvested), invested,
loan,
payout, payout,
netPayout,
profit, profit,
profitPercent, profitPercent,
totalShares, totalShares,
@ -191,8 +238,8 @@ export function getContractBetMetrics(contract: Contract, yourBets: Bet[]) {
export function getContractBetNullMetrics() { export function getContractBetNullMetrics() {
return { return {
invested: 0, invested: 0,
loan: 0,
payout: 0, payout: 0,
netPayout: 0,
profit: 0, profit: 0,
profitPercent: 0, profitPercent: 0,
totalShares: {} as { [outcome: string]: number }, totalShares: {} as { [outcome: string]: number },
@ -200,7 +247,9 @@ export function getContractBetNullMetrics() {
} }
} }
export function getTopAnswer(contract: FreeResponseContract) { export function getTopAnswer(
contract: FreeResponseContract | MultipleChoiceContract
) {
const { answers } = contract const { answers } = contract
const top = maxBy( const top = maxBy(
answers?.map((answer) => ({ answers?.map((answer) => ({
@ -211,3 +260,43 @@ export function getTopAnswer(contract: FreeResponseContract) {
) )
return top?.answer return top?.answer
} }
export function getLargestPosition(contract: Contract, userBets: Bet[]) {
let yesFloorShares = 0,
yesShares = 0,
noShares = 0,
noFloorShares = 0
if (userBets.length === 0) {
return null
}
if (contract.outcomeType === 'FREE_RESPONSE') {
const answerCounts: { [outcome: string]: number } = {}
for (const bet of userBets) {
if (bet.outcome) {
if (!answerCounts[bet.outcome]) {
answerCounts[bet.outcome] = bet.amount
} else {
answerCounts[bet.outcome] += bet.amount
}
}
}
const majorityAnswer =
maxBy(Object.keys(answerCounts), (outcome) => answerCounts[outcome]) ?? ''
return {
prob: undefined,
shares: answerCounts[majorityAnswer] || 0,
outcome: majorityAnswer,
}
}
const [yesBets, noBets] = partition(userBets, (bet) => bet.outcome === 'YES')
yesShares = sumBy(yesBets, (bet) => bet.shares)
noShares = sumBy(noBets, (bet) => bet.shares)
yesFloorShares = Math.floor(yesShares)
noFloorShares = Math.floor(noShares)
const shares = yesFloorShares || noFloorShares
const outcome = yesFloorShares > noFloorShares ? 'YES' : 'NO'
return { shares, outcome }
}

View File

@ -1,6 +1,7 @@
import { difference } from 'lodash' import { difference } from 'lodash'
export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default' export const CATEGORIES_GROUP_SLUG_POSTFIX = '-default'
export const CATEGORIES = { export const CATEGORIES = {
politics: 'Politics', politics: 'Politics',
technology: 'Technology', technology: 'Technology',
@ -30,10 +31,13 @@ export const EXCLUDED_CATEGORIES: category[] = [
'manifold', 'manifold',
'personal', 'personal',
'covid', 'covid',
'culture',
'gaming', 'gaming',
'crypto', 'crypto',
'world',
] ]
export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES) export const DEFAULT_CATEGORIES = difference(CATEGORY_LIST, EXCLUDED_CATEGORIES)
export const DEFAULT_CATEGORY_GROUPS = DEFAULT_CATEGORIES.map((c) => ({
slug: c.toLowerCase() + CATEGORIES_GROUP_SLUG_POSTFIX,
name: CATEGORIES[c as category],
}))

65
common/challenge.ts Normal file
View File

@ -0,0 +1,65 @@
import { IS_PRIVATE_MANIFOLD } from './envs/constants'
export type Challenge = {
// The link to send: https://manifold.markets/challenges/username/market-slug/{slug}
// Also functions as the unique id for the link.
slug: string
// The user that created the challenge.
creatorId: string
creatorUsername: string
creatorName: string
creatorAvatarUrl?: string
// Displayed to people claiming the challenge
message: string
// How much to put up
creatorAmount: number
// YES or NO for now
creatorOutcome: string
// Different than the creator
acceptorOutcome: string
acceptorAmount: number
// The probability the challenger thinks
creatorOutcomeProb: number
contractId: string
contractSlug: string
contractQuestion: string
contractCreatorUsername: string
createdTime: number
// If null, the link is valid forever
expiresTime: number | null
// How many times the challenge can be used
maxUses: number
// Used for simpler caching
acceptedByUserIds: string[]
// Successful redemptions of the link
acceptances: Acceptance[]
// TODO: will have to fill this on resolve contract
isResolved: boolean
resolutionOutcome?: string
}
export type Acceptance = {
// User that accepted the challenge
userId: string
userUsername: string
userName: string
userAvatarUrl: string
// The ID of the successful bet that tracks the money moved
betId: string
createdTime: number
}
export const CHALLENGES_ENABLED = !IS_PRIVATE_MANIFOLD

View File

@ -169,7 +169,7 @@ export const charities: Charity[] = [
{ {
name: "Founder's Pledge Climate Change Fund", name: "Founder's Pledge Climate Change Fund",
website: 'https://founderspledge.com/funds/climate-change-fund', website: 'https://founderspledge.com/funds/climate-change-fund',
photo: 'https://i.imgur.com/ZAhzHu4.png', photo: 'https://i.imgur.com/9turaJW.png',
preview: preview:
'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.', 'The Climate Change Fund aims to sustainably reach net-zero emissions globally, while still allowing growth to free millions from energy poverty.',
description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally. description: `The Climate Change Fund aims to sustainably reach net-zero emissions globally.
@ -183,7 +183,7 @@ export const charities: Charity[] = [
{ {
name: "Founder's Pledge Patient Philanthropy Fund", name: "Founder's Pledge Patient Philanthropy Fund",
website: 'https://founderspledge.com/funds/patient-philanthropy-fund', website: 'https://founderspledge.com/funds/patient-philanthropy-fund',
photo: 'https://i.imgur.com/ZAhzHu4.png', photo: 'https://i.imgur.com/LLR6CI6.png',
preview: preview:
'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity', 'The Patient Philanthropy Project aims to safeguard and benefit the long-term future of humanity',
description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future. description: `The Patient Philanthropy Project focuses on how we can collectively grow our resources to support the long-term flourishing of humanity. It addresses a crucial gap: as a society, we spend much too little on safeguarding and benefiting future generations. In fact, we spend more money on ice cream each year than we do on preventing our own extinction. However, people in the future - who do not have a voice in their future survival or environment - matter. Lots of them may yet come into existence and we have the ability to positively affect their lives now, if only by making sure we avoid major catastrophes that could destroy our common future.
@ -551,6 +551,53 @@ With an emphasis on approval voting, we bring better elections to people across
The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that dont represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`, The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that dont represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`,
}, },
{
name: 'Founders Pledge Global Health and Development Fund',
website: 'https://founderspledge.com/funds/global-health-and-development',
photo: 'https://i.imgur.com/EXbxH7T.png',
preview:
'Tackling the vast global inequalities in health, wealth and opportunity',
description: `Nearly half the world lives on less than $2.50 a day, yet giving by the worlds richest often overlooks the worlds poorest and most vulnerable. Despite the average American household being richer than 90% of the rest of the world, only 6% of US charitable giving goes to charities which work internationally.
This Fund is focused on helping those who need it most, wherever that help can make the biggest difference. By building a mixed portfolio of direct and indirect interventions, such as policy work, we aim to:
Improve the lives of the world's most vulnerable people.
Reduce the number of easily preventable deaths worldwide.
Work towards sustainable, systemic change.`,
},
{
name: 'YIMBY Law',
website: 'https://www.yimbylaw.org/',
photo: 'https://i.imgur.com/zlzp21Z.png',
preview:
'YIMBY Law works to make housing in California more accessible and affordable, by enforcing state housing laws.',
description: `
YIMBY Law works to make housing in California more accessible and affordable. Our method is to enforce state housing laws, and some examples are outlined below. We send letters to cities considering zoning or general plan compliant housing developments informing them of their duties under state law, and sue them when they do not comply.
If you would like to support our work, you can do so by getting involved or by donating.`,
},
{
name: 'CaRLA',
website: 'https://carlaef.org/',
photo: 'https://i.imgur.com/IsNVTOY.png',
preview:
'The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.',
description: `
The California Renters Legal Advocacy and Education Funds core mission is to make lasting impacts to improve the affordability and accessibility of housing to current and future Californians, especially low- and moderate-income people and communities of color.
CaRLA uses legal advocacy and education to ensure all cities comply with their own zoning and state housing laws and do their part to help solve the states housing shortage.
In addition to housing impact litigation, we provide free legal aid, education and workshops, counseling and advocacy to advocates, homeowners, small developers, and city and state government officials.`,
},
{
name: 'Mriya',
website: 'https://mriya-ua.org/',
photo:
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2Fdefault%2Fci2h3hStFM.47?alt=media&token=0d2cdc3d-e4d8-4f5e-8f23-4a586b6ff637',
preview: 'Donate supplies to soldiers in Ukraine',
description:
'Donate supplies to soldiers in Ukraine, including tourniquets and plate carriers.',
},
].map((charity) => { ].map((charity) => {
const slug = charity.name.toLowerCase().replace(/\s/g, '-') const slug = charity.name.toLowerCase().replace(/\s/g, '-')
return { return {

View File

@ -1,19 +1,56 @@
import type { JSONContent } from '@tiptap/core'
export type AnyCommentType = OnContract | OnGroup | OnPost
// Currently, comments are created after the bet, not atomically with the bet. // Currently, comments are created after the bet, not atomically with the bet.
// They're uniquely identified by the pair contractId/betId. // They're uniquely identified by the pair contractId/betId.
export type Comment = { export type Comment<T extends AnyCommentType = AnyCommentType> = {
id: string id: string
contractId?: string
groupId?: string
betId?: string
answerOutcome?: string
replyToCommentId?: string replyToCommentId?: string
userId: string userId: string
text: string /** @deprecated - content now stored as JSON in content*/
text?: string
content: JSONContent
createdTime: number createdTime: number
// Denormalized, for rendering comments // Denormalized, for rendering comments
userName: string userName: string
userUsername: string userUsername: string
userAvatarUrl?: string userAvatarUrl?: string
bountiesAwarded?: number
} & T
export type OnContract = {
commentType: 'contract'
contractId: string
answerOutcome?: string
betId?: string
// denormalized from contract
contractSlug: string
contractQuestion: string
// denormalized from bet
betAmount?: number
betOutcome?: string
// denormalized based on betting history
commenterPositionProb?: number // binary only
commenterPositionShares?: number
commenterPositionOutcome?: string
} }
export type OnGroup = {
commentType: 'group'
groupId: string
}
export type OnPost = {
commentType: 'post'
postId: string
}
export type ContractComment = Comment<OnContract>
export type GroupComment = Comment<OnGroup>
export type PostComment = Comment<OnPost>

168
common/contract-details.ts Normal file
View File

@ -0,0 +1,168 @@
import { Challenge } from './challenge'
import { BinaryContract, Contract } from './contract'
import { getFormattedMappedValue } from './pseudo-numeric'
import { getProbability } from './calculate'
import { richTextToString } from './util/parse'
import { getCpmmProbability } from './calculate-cpmm'
import { getDpmProbability } from './calculate-dpm'
import { formatMoney, formatPercent } from './util/format'
export function contractMetrics(contract: Contract) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dayjs = require('dayjs')
const { createdTime, resolutionTime, isResolved } = contract
const createdDate = dayjs(createdTime).format('MMM D')
const resolvedDate = isResolved
? dayjs(resolutionTime).format('MMM D')
: undefined
const volumeLabel = `${formatMoney(contract.volume)} bet`
return { volumeLabel, createdDate, resolvedDate }
}
// String version of the above, to send to the OpenGraph image generator
export function contractTextDetails(contract: Contract) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dayjs = require('dayjs')
const { closeTime, groupLinks } = contract
const { createdDate, resolvedDate, volumeLabel } = contractMetrics(contract)
const groupHashtags = groupLinks?.map((g) => `#${g.name.replace(/ /g, '')}`)
return (
`${resolvedDate ? `${createdDate} - ${resolvedDate}` : createdDate}` +
(closeTime
? `${closeTime > Date.now() ? 'Closes' : 'Closed'} ${dayjs(
closeTime
).format('MMM D, h:mma')}`
: '') +
`${volumeLabel}` +
(groupHashtags ? `${groupHashtags.join(' ')}` : '')
)
}
export function getBinaryProb(contract: BinaryContract) {
const { pool, resolutionProbability, mechanism } = contract
return (
resolutionProbability ??
(mechanism === 'cpmm-1'
? getCpmmProbability(pool, contract.p)
: getDpmProbability(contract.totalShares))
)
}
export const getOpenGraphProps = (contract: Contract) => {
const {
resolution,
question,
creatorName,
creatorUsername,
outcomeType,
creatorAvatarUrl,
description: desc,
} = contract
const probPercent =
outcomeType === 'BINARY'
? formatPercent(getBinaryProb(contract))
: undefined
const numericValue =
outcomeType === 'PSEUDO_NUMERIC'
? getFormattedMappedValue(contract)(getProbability(contract))
: undefined
const stringDesc = typeof desc === 'string' ? desc : richTextToString(desc)
const description = resolution
? `Resolved ${resolution}. ${stringDesc}`
: probPercent
? `${probPercent} chance. ${stringDesc}`
: stringDesc
return {
question,
probability: probPercent,
metadata: contractTextDetails(contract),
creatorName,
creatorUsername,
creatorAvatarUrl,
description,
numericValue,
resolution,
}
}
export type OgCardProps = {
question: string
probability?: string
metadata: string
creatorName: string
creatorUsername: string
creatorAvatarUrl?: string
numericValue?: string
resolution?: string
}
export function buildCardUrl(props: OgCardProps, challenge?: Challenge) {
const {
creatorAmount,
acceptances,
acceptorAmount,
creatorOutcome,
acceptorOutcome,
} = challenge || {}
const {
probability,
numericValue,
resolution,
creatorAvatarUrl,
question,
metadata,
creatorUsername,
creatorName,
} = props
const { userName, userAvatarUrl } = acceptances?.[0] ?? {}
const probabilityParam =
probability === undefined
? ''
: `&probability=${encodeURIComponent(probability ?? '')}`
const numericValueParam =
numericValue === undefined
? ''
: `&numericValue=${encodeURIComponent(numericValue ?? '')}`
const creatorAvatarUrlParam =
creatorAvatarUrl === undefined
? ''
: `&creatorAvatarUrl=${encodeURIComponent(creatorAvatarUrl ?? '')}`
const challengeUrlParams = challenge
? `&creatorAmount=${creatorAmount}&creatorOutcome=${creatorOutcome}` +
`&challengerAmount=${acceptorAmount}&challengerOutcome=${acceptorOutcome}` +
`&acceptedName=${userName ?? ''}&acceptedAvatarUrl=${userAvatarUrl ?? ''}`
: ''
const resolutionUrlParam = resolution
? `&resolution=${encodeURIComponent(resolution)}`
: ''
// URL encode each of the props, then add them as query params
return (
`https://manifold-og-image.vercel.app/m.png` +
`?question=${encodeURIComponent(question)}` +
probabilityParam +
numericValueParam +
`&metadata=${encodeURIComponent(metadata)}` +
`&creatorName=${encodeURIComponent(creatorName)}` +
creatorAvatarUrlParam +
`&creatorUsername=${encodeURIComponent(creatorUsername)}` +
challengeUrlParams +
resolutionUrlParam
)
}

View File

@ -1,15 +1,23 @@
import { Answer } from './answer' import { Answer } from './answer'
import { Fees } from './fees' import { Fees } from './fees'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
import { GroupLink } from 'common/group'
export type AnyMechanism = DPM | CPMM export type AnyMechanism = DPM | CPMM
export type AnyOutcomeType = Binary | PseudoNumeric | FreeResponse | Numeric export type AnyOutcomeType =
| Binary
| MultipleChoice
| PseudoNumeric
| FreeResponse
| Numeric
export type AnyContractType = export type AnyContractType =
| (CPMM & Binary) | (CPMM & Binary)
| (CPMM & PseudoNumeric) | (CPMM & PseudoNumeric)
| (DPM & Binary) | (DPM & Binary)
| (DPM & FreeResponse) | (DPM & FreeResponse)
| (DPM & Numeric) | (DPM & Numeric)
| (DPM & MultipleChoice)
export type Contract<T extends AnyContractType = AnyContractType> = { export type Contract<T extends AnyContractType = AnyContractType> = {
id: string id: string
@ -24,7 +32,7 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
description: string | JSONContent // More info about what the contract is about description: string | JSONContent // More info about what the contract is about
tags: string[] tags: string[]
lowercaseTags: string[] lowercaseTags: string[]
visibility: 'public' | 'unlisted' visibility: visibility
createdTime: number // Milliseconds since epoch createdTime: number // Milliseconds since epoch
lastUpdatedTime?: number // Updated on new bet or comment lastUpdatedTime?: number // Updated on new bet or comment
@ -42,18 +50,30 @@ export type Contract<T extends AnyContractType = AnyContractType> = {
volume: number volume: number
volume24Hours: number volume24Hours: number
volume7Days: number volume7Days: number
elasticity: number
collectedFees: Fees collectedFees: Fees
groupSlugs?: string[] groupSlugs?: string[]
groupLinks?: GroupLink[]
uniqueBettorIds?: string[] uniqueBettorIds?: string[]
uniqueBettorCount?: number uniqueBettorCount?: number
popularityScore?: number
dailyScore?: number
followerCount?: number
featuredOnHomeRank?: number
likedByUserIds?: string[]
likedByUserCount?: number
flaggedByUsernames?: string[]
openCommentBounties?: number
unlistedById?: string
} & T } & T
export type BinaryContract = Contract & Binary export type BinaryContract = Contract & Binary
export type PseudoNumericContract = Contract & PseudoNumeric export type PseudoNumericContract = Contract & PseudoNumeric
export type NumericContract = Contract & Numeric export type NumericContract = Contract & Numeric
export type FreeResponseContract = Contract & FreeResponse export type FreeResponseContract = Contract & FreeResponse
export type MultipleChoiceContract = Contract & MultipleChoice
export type DPMContract = Contract & DPM export type DPMContract = Contract & DPM
export type CPMMContract = Contract & CPMM export type CPMMContract = Contract & CPMM
export type DPMBinaryContract = BinaryContract & DPM export type DPMBinaryContract = BinaryContract & DPM
@ -72,7 +92,14 @@ export type CPMM = {
mechanism: 'cpmm-1' mechanism: 'cpmm-1'
pool: { [outcome: string]: number } pool: { [outcome: string]: number }
p: number // probability constant in y^p * n^(1-p) = k p: number // probability constant in y^p * n^(1-p) = k
totalLiquidity: number // in M$ totalLiquidity: number // for historical reasons, this the total subsidy amount added in M$
subsidyPool: number // current value of subsidy pool in M$
prob: number
probChanges: {
day: number
week: number
month: number
}
} }
export type Binary = { export type Binary = {
@ -101,6 +128,13 @@ export type FreeResponse = {
resolutions?: { [outcome: string]: number } // Used for MKT resolution. resolutions?: { [outcome: string]: number } // Used for MKT resolution.
} }
export type MultipleChoice = {
outcomeType: 'MULTIPLE_CHOICE'
answers: Answer[]
resolution?: string | 'MKT' | 'CANCEL'
resolutions?: { [outcome: string]: number } // Used for MKT resolution.
}
export type Numeric = { export type Numeric = {
outcomeType: 'NUMERIC' outcomeType: 'NUMERIC'
bucketCount: number bucketCount: number
@ -115,13 +149,17 @@ export type resolution = 'YES' | 'NO' | 'MKT' | 'CANCEL'
export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const export const RESOLUTIONS = ['YES', 'NO', 'MKT', 'CANCEL'] as const
export const OUTCOME_TYPES = [ export const OUTCOME_TYPES = [
'BINARY', 'BINARY',
'MULTIPLE_CHOICE',
'FREE_RESPONSE', 'FREE_RESPONSE',
'PSEUDO_NUMERIC', 'PSEUDO_NUMERIC',
'NUMERIC', 'NUMERIC',
] as const ] as const
export const MAX_QUESTION_LENGTH = 480 export const MAX_QUESTION_LENGTH = 240
export const MAX_DESCRIPTION_LENGTH = 10000 export const MAX_DESCRIPTION_LENGTH = 16000
export const MAX_TAG_LENGTH = 60 export const MAX_TAG_LENGTH = 60
export const CPMM_MIN_POOL_QTY = 0.01 export const CPMM_MIN_POOL_QTY = 0.01
export type visibility = 'public' | 'unlisted'
export const VISIBILITIES = ['public', 'unlisted'] as const

20
common/economy.ts Normal file
View File

@ -0,0 +1,20 @@
import { ENV_CONFIG } from './envs/constants'
const econ = ENV_CONFIG.economy
export const FIXED_ANTE = econ?.FIXED_ANTE ?? 100
export const STARTING_BALANCE = econ?.STARTING_BALANCE ?? 1000
// for sus users, i.e. multiple sign ups for same person
export const SUS_STARTING_BALANCE = econ?.SUS_STARTING_BALANCE ?? 10
export const REFERRAL_AMOUNT = econ?.REFERRAL_AMOUNT ?? 250
export const UNIQUE_BETTOR_BONUS_AMOUNT = econ?.UNIQUE_BETTOR_BONUS_AMOUNT ?? 10
export const BETTING_STREAK_BONUS_AMOUNT =
econ?.BETTING_STREAK_BONUS_AMOUNT ?? 5
export const BETTING_STREAK_BONUS_MAX = econ?.BETTING_STREAK_BONUS_MAX ?? 25
export const BETTING_STREAK_RESET_HOUR = econ?.BETTING_STREAK_RESET_HOUR ?? 7
export const FREE_MARKETS_PER_USER_MAX = econ?.FREE_MARKETS_PER_USER_MAX ?? 5
export const COMMENT_BOUNTY_AMOUNT = econ?.COMMENT_BOUNTY_AMOUNT ?? 250
export const UNIQUE_BETTOR_LIQUIDITY = 20

View File

@ -21,15 +21,27 @@ export function isWhitelisted(email?: string) {
} }
// TODO: Before open sourcing, we should turn these into env vars // TODO: Before open sourcing, we should turn these into env vars
export function isAdmin(email: string) { export function isAdmin(email?: string) {
if (!email) {
return false
}
return ENV_CONFIG.adminEmails.includes(email) return ENV_CONFIG.adminEmails.includes(email)
} }
export function isManifoldId(userId: string) {
return userId === 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2'
}
export const DOMAIN = ENV_CONFIG.domain export const DOMAIN = ENV_CONFIG.domain
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE' export const IS_PRIVATE_MANIFOLD = ENV_CONFIG.visibility === 'PRIVATE'
export const AUTH_COOKIE_NAME = `FBUSER_${PROJECT_ID.toUpperCase().replace(
/-/g,
'_'
)}`
// Manifold's domain or any subdomains thereof // Manifold's domain or any subdomains thereof
export const CORS_ORIGIN_MANIFOLD = new RegExp( export const CORS_ORIGIN_MANIFOLD = new RegExp(
'^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$' '^https?://(?:[a-zA-Z0-9\\-]+\\.)*' + escapeRegExp(ENV_CONFIG.domain) + '$'
@ -40,3 +52,7 @@ export const CORS_ORIGIN_VERCEL = new RegExp(
) )
// Any localhost server on any port // Any localhost server on any port
export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/ export const CORS_ORIGIN_LOCALHOST = /^http:\/\/localhost:\d+$/
export function firestoreConsolePath(contractId: string) {
return `https://console.firebase.google.com/project/${PROJECT_ID}/firestore/data/~2Fcontracts~2F${contractId}`
}

View File

@ -2,6 +2,7 @@ import { EnvConfig, PROD_CONFIG } from './prod'
export const DEV_CONFIG: EnvConfig = { export const DEV_CONFIG: EnvConfig = {
...PROD_CONFIG, ...PROD_CONFIG,
domain: 'dev.manifold.markets',
firebaseConfig: { firebaseConfig: {
apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw', apiKey: 'AIzaSyBoq3rzUa8Ekyo3ZaTnlycQYPRCA26VpOw',
authDomain: 'dev-mantic-markets.firebaseapp.com', authDomain: 'dev-mantic-markets.firebaseapp.com',
@ -15,4 +16,6 @@ export const DEV_CONFIG: EnvConfig = {
cloudRunId: 'w3txbmd3ba', cloudRunId: 'w3txbmd3ba',
cloudRunRegion: 'uc', cloudRunRegion: 'uc',
amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3', amplitudeApiKey: 'fd8cbfd964b9a205b8678a39faae71b3',
twitchBotEndpoint: 'https://dev-twitch-bot.manifold.markets',
sprigEnvironmentId: 'Tu7kRZPm7daP',
} }

View File

@ -2,6 +2,8 @@ export type EnvConfig = {
domain: string domain: string
firebaseConfig: FirebaseConfig firebaseConfig: FirebaseConfig
amplitudeApiKey?: string amplitudeApiKey?: string
twitchBotEndpoint?: string
sprigEnvironmentId?: string
// IDs for v2 cloud functions -- find these by deploying a cloud function and // IDs for v2 cloud functions -- find these by deploying a cloud function and
// examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app // examining the URL, https://[name]-[cloudRunId]-[cloudRunRegion].a.run.app
@ -15,14 +17,31 @@ export type EnvConfig = {
// Branding // Branding
moneyMoniker: string // e.g. 'M$' moneyMoniker: string // e.g. 'M$'
bettor?: string // e.g. 'bettor' or 'predictor'
presentBet?: string // e.g. 'bet' or 'predict'
pastBet?: string // e.g. 'bet' or 'prediction'
faviconPath?: string // Should be a file in /public faviconPath?: string // Should be a file in /public
navbarLogoPath?: string navbarLogoPath?: string
newQuestionPlaceholders: string[] newQuestionPlaceholders: string[]
// Currency controls economy?: Economy
fixedAnte?: number }
startingBalance?: number
referralBonus?: number export type Economy = {
FIXED_ANTE?: number
STARTING_BALANCE?: number
SUS_STARTING_BALANCE?: number
REFERRAL_AMOUNT?: number
UNIQUE_BETTOR_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_AMOUNT?: number
BETTING_STREAK_BONUS_MAX?: number
BETTING_STREAK_RESET_HOUR?: number
FREE_MARKETS_PER_USER_MAX?: number
COMMENT_BOUNTY_AMOUNT?: number
} }
type FirebaseConfig = { type FirebaseConfig = {
@ -39,6 +58,7 @@ type FirebaseConfig = {
export const PROD_CONFIG: EnvConfig = { export const PROD_CONFIG: EnvConfig = {
domain: 'manifold.markets', domain: 'manifold.markets',
amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15', amplitudeApiKey: '2d6509fd4185ebb8be29709842752a15',
sprigEnvironmentId: 'sQcrq9TDqkib',
firebaseConfig: { firebaseConfig: {
apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw', apiKey: 'AIzaSyDp3J57vLeAZCzxLD-vcPaGIkAmBoGOSYw',
@ -50,6 +70,7 @@ export const PROD_CONFIG: EnvConfig = {
appId: '1:128925704902:web:f61f86944d8ffa2a642dc7', appId: '1:128925704902:web:f61f86944d8ffa2a642dc7',
measurementId: 'G-SSFK1Q138D', measurementId: 'G-SSFK1Q138D',
}, },
twitchBotEndpoint: 'https://twitch-bot.manifold.markets',
cloudRunId: 'nggbo3neva', cloudRunId: 'nggbo3neva',
cloudRunRegion: 'uc', cloudRunRegion: 'uc',
adminEmails: [ adminEmails: [
@ -58,10 +79,17 @@ export const PROD_CONFIG: EnvConfig = {
'taowell@gmail.com', // Stephen 'taowell@gmail.com', // Stephen
'abc.sinclair@gmail.com', // Sinclair 'abc.sinclair@gmail.com', // Sinclair
'manticmarkets@gmail.com', // Manifold 'manticmarkets@gmail.com', // Manifold
'iansphilips@gmail.com', // Ian
'd4vidchee@gmail.com', // D4vid
'federicoruizcassarino@gmail.com', // Fede
'ingawei@gmail.com', //Inga
], ],
visibility: 'PUBLIC', visibility: 'PUBLIC',
moneyMoniker: 'M$', moneyMoniker: 'M$',
bettor: 'trader',
pastBet: 'trade',
presentBet: 'trade',
navbarLogoPath: '', navbarLogoPath: '',
faviconPath: '/favicon.ico', faviconPath: '/favicon.ico',
newQuestionPlaceholders: [ newQuestionPlaceholders: [

View File

@ -1,9 +1,11 @@
export const FLAT_TRADE_FEE = 0.1 // M$0.1
export const PLATFORM_FEE = 0 export const PLATFORM_FEE = 0
export const CREATOR_FEE = 0.1 export const CREATOR_FEE = 0
export const LIQUIDITY_FEE = 0 export const LIQUIDITY_FEE = 0
export const DPM_PLATFORM_FEE = 0.01 export const DPM_PLATFORM_FEE = 0.0
export const DPM_CREATOR_FEE = 0.04 export const DPM_CREATOR_FEE = 0.0
export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE export const DPM_FEES = DPM_PLATFORM_FEE + DPM_CREATOR_FEE
export type Fees = { export type Fees = {

View File

@ -2,3 +2,8 @@ export type Follow = {
userId: string userId: string
timestamp: number timestamp: number
} }
export type ContractFollow = {
id: string // user id
createdTime: number
}

3
common/globalConfig.ts Normal file
View File

@ -0,0 +1,3 @@
export type GlobalConfig = {
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
}

View File

@ -6,16 +6,37 @@ export type Group = {
creatorId: string // User id creatorId: string // User id
createdTime: number createdTime: number
mostRecentActivityTime: number mostRecentActivityTime: number
memberIds: string[] // User ids
anyoneCanJoin: boolean anyoneCanJoin: boolean
contractIds: string[] totalContracts: number
totalMembers: number
aboutPostId?: string
postIds: string[]
chatDisabled?: boolean chatDisabled?: boolean
mostRecentChatActivityTime?: number
mostRecentContractAddedTime?: number mostRecentContractAddedTime?: number
cachedLeaderboard?: {
topTraders: {
userId: string
score: number
}[]
topCreators: {
userId: string
score: number
}[]
}
pinnedItems: { itemId: string; type: 'post' | 'contract' }[]
} }
export const MAX_GROUP_NAME_LENGTH = 75 export const MAX_GROUP_NAME_LENGTH = 75
export const MAX_ABOUT_LENGTH = 140 export const MAX_ABOUT_LENGTH = 140
export const MAX_ID_LENGTH = 60 export const MAX_ID_LENGTH = 60
export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome']
export const GROUP_CHAT_SLUG = 'chat' export const GROUP_CHAT_SLUG = 'chat'
export type GroupLink = {
slug: string
name: string
groupId: string
createdTime: number
userId?: string
}
export type GroupContractDoc = { contractId: string; createdTime: number }

9
common/like.ts Normal file
View File

@ -0,0 +1,9 @@
export type Like = {
id: string // will be id of the object liked, i.e. contract.id
userId: string
type: 'contract' | 'post'
createdTime: number
tipTxnId?: string // only holds most recent tip txn id
}
export const LIKE_TIP_AMOUNT = 10
export const TIP_UNDO_DURATION = 2000

138
common/loans.ts Normal file
View File

@ -0,0 +1,138 @@
import { Dictionary, groupBy, sumBy, minBy } from 'lodash'
import { Bet } from './bet'
import { getContractBetMetrics } from './calculate'
import {
Contract,
CPMMContract,
FreeResponseContract,
MultipleChoiceContract,
} from './contract'
import { PortfolioMetrics, User } from './user'
import { filterDefined } from './util/array'
const LOAN_DAILY_RATE = 0.02
const calculateNewLoan = (investedValue: number, loanTotal: number) => {
const netValue = investedValue - loanTotal
return netValue * LOAN_DAILY_RATE
}
export const getLoanUpdates = (
users: User[],
contractsById: { [contractId: string]: Contract },
portfolioByUser: { [userId: string]: PortfolioMetrics | undefined },
betsByUser: { [userId: string]: Bet[] }
) => {
const eligibleUsers = filterDefined(
users.map((user) =>
isUserEligibleForLoan(portfolioByUser[user.id]) ? user : undefined
)
)
const betUpdates = eligibleUsers
.map((user) => {
const updates = calculateLoanBetUpdates(
betsByUser[user.id] ?? [],
contractsById
).betUpdates
return updates.map((update) => ({ ...update, user }))
})
.flat()
const updatesByUser = groupBy(betUpdates, (update) => update.userId)
const userPayouts = Object.values(updatesByUser).map((updates) => {
return {
user: updates[0].user,
payout: sumBy(updates, (update) => update.newLoan),
}
})
return {
betUpdates,
userPayouts,
}
}
const isUserEligibleForLoan = (portfolio: PortfolioMetrics | undefined) => {
if (!portfolio) return true
const { balance, investmentValue } = portfolio
return balance + investmentValue > 0
}
const calculateLoanBetUpdates = (
bets: Bet[],
contractsById: Dictionary<Contract>
) => {
const betsByContract = groupBy(bets, (bet) => bet.contractId)
const contracts = filterDefined(
Object.keys(betsByContract).map((contractId) => contractsById[contractId])
).filter((c) => !c.isResolved)
const betUpdates = filterDefined(
contracts
.map((c) => {
if (c.mechanism === 'cpmm-1') {
return getBinaryContractLoanUpdate(c, betsByContract[c.id])
} else if (
c.outcomeType === 'FREE_RESPONSE' ||
c.outcomeType === 'MULTIPLE_CHOICE'
)
return getFreeResponseContractLoanUpdate(c, betsByContract[c.id])
else {
// Unsupported contract / mechanism for loans.
return []
}
})
.flat()
)
const totalNewLoan = sumBy(betUpdates, (loanUpdate) => loanUpdate.loanTotal)
return {
totalNewLoan,
betUpdates,
}
}
const getBinaryContractLoanUpdate = (contract: CPMMContract, bets: Bet[]) => {
const { invested } = getContractBetMetrics(contract, bets)
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const oldestBet = minBy(bets, (bet) => bet.createdTime)
const newLoan = calculateNewLoan(invested, loanAmount)
if (!isFinite(newLoan) || newLoan <= 0 || !oldestBet) return undefined
const loanTotal = (oldestBet.loanAmount ?? 0) + newLoan
return {
userId: oldestBet.userId,
contractId: contract.id,
betId: oldestBet.id,
newLoan,
loanTotal,
}
}
const getFreeResponseContractLoanUpdate = (
contract: FreeResponseContract | MultipleChoiceContract,
bets: Bet[]
) => {
const openBets = bets.filter((bet) => !bet.isSold && !bet.sale)
return openBets.map((bet) => {
const loanAmount = bet.loanAmount ?? 0
const newLoan = calculateNewLoan(bet.amount, loanAmount)
const loanTotal = loanAmount + newLoan
if (!isFinite(newLoan) || newLoan <= 0) return undefined
return {
userId: bet.userId,
contractId: contract.id,
betId: bet.id,
newLoan,
loanTotal,
}
})
}

View File

@ -1,6 +1,6 @@
import { sortBy, sumBy } from 'lodash' import { sortBy, sum, sumBy } from 'lodash'
import { Bet, fill, LimitBet, MAX_LOAN_PER_CONTRACT, NumericBet } from './bet' import { Bet, fill, LimitBet, NumericBet } from './bet'
import { import {
calculateDpmShares, calculateDpmShares,
getDpmProbability, getDpmProbability,
@ -17,7 +17,7 @@ import {
import { import {
CPMMBinaryContract, CPMMBinaryContract,
DPMBinaryContract, DPMBinaryContract,
FreeResponseContract, DPMContract,
NumericContract, NumericContract,
PseudoNumericContract, PseudoNumericContract,
} from './contract' } from './contract'
@ -30,7 +30,10 @@ import {
floatingLesserEqual, floatingLesserEqual,
} from './util/math' } from './util/math'
export type CandidateBet<T extends Bet = Bet> = Omit<T, 'id' | 'userId'> export type CandidateBet<T extends Bet = Bet> = Omit<
T,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
>
export type BetInfo = { export type BetInfo = {
newBet: CandidateBet newBet: CandidateBet
newPool?: { [outcome: string]: number } newPool?: { [outcome: string]: number }
@ -140,8 +143,16 @@ export const computeFills = (
betAmount: number, betAmount: number,
state: CpmmState, state: CpmmState,
limitProb: number | undefined, limitProb: number | undefined,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => { ) => {
if (isNaN(betAmount)) {
throw new Error('Invalid bet amount: ${betAmount}')
}
if (isNaN(limitProb ?? 0)) {
throw new Error('Invalid limitProb: ${limitProb}')
}
const sortedBets = sortBy( const sortedBets = sortBy(
unfilledBets.filter((bet) => bet.outcome !== outcome), unfilledBets.filter((bet) => bet.outcome !== outcome),
(bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb), (bet) => (outcome === 'YES' ? bet.limitProb : -bet.limitProb),
@ -155,10 +166,12 @@ export const computeFills = (
shares: number shares: number
timestamp: number timestamp: number
}[] = [] }[] = []
const ordersToCancel: LimitBet[] = []
let amount = betAmount let amount = betAmount
let cpmmState = { pool: state.pool, p: state.p } let cpmmState = { pool: state.pool, p: state.p }
let totalFees = noFees let totalFees = noFees
const currentBalanceByUserId = { ...balanceByUserId }
let i = 0 let i = 0
while (true) { while (true) {
@ -175,9 +188,20 @@ export const computeFills = (
takers.push(taker) takers.push(taker)
} else { } else {
// Matched against bet. // Matched against bet.
i++
const { userId } = maker.bet
const makerBalance = currentBalanceByUserId[userId]
if (floatingGreaterEqual(makerBalance, maker.amount)) {
currentBalanceByUserId[userId] = makerBalance - maker.amount
} else {
// Insufficient balance. Cancel maker bet.
ordersToCancel.push(maker.bet)
continue
}
takers.push(taker) takers.push(taker)
makers.push(maker) makers.push(maker)
i++
} }
amount -= taker.amount amount -= taker.amount
@ -185,7 +209,7 @@ export const computeFills = (
if (floatingEqual(amount, 0)) break if (floatingEqual(amount, 0)) break
} }
return { takers, makers, totalFees, cpmmState } return { takers, makers, totalFees, cpmmState, ordersToCancel }
} }
export const getBinaryCpmmBetInfo = ( export const getBinaryCpmmBetInfo = (
@ -193,15 +217,17 @@ export const getBinaryCpmmBetInfo = (
betAmount: number, betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract, contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number | undefined, limitProb: number | undefined,
unfilledBets: LimitBet[] unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => { ) => {
const { pool, p } = contract const { pool, p } = contract
const { takers, makers, cpmmState, totalFees } = computeFills( const { takers, makers, cpmmState, totalFees, ordersToCancel } = computeFills(
outcome, outcome,
betAmount, betAmount,
{ pool, p }, { pool, p },
limitProb, limitProb,
unfilledBets unfilledBets,
balanceByUserId
) )
const probBefore = getCpmmProbability(contract.pool, contract.p) const probBefore = getCpmmProbability(contract.pool, contract.p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
@ -236,14 +262,42 @@ export const getBinaryCpmmBetInfo = (
newP: cpmmState.p, newP: cpmmState.p,
newTotalLiquidity, newTotalLiquidity,
makers, makers,
ordersToCancel,
} }
} }
export const getBinaryBetStats = (
outcome: 'YES' | 'NO',
betAmount: number,
contract: CPMMBinaryContract | PseudoNumericContract,
limitProb: number,
unfilledBets: LimitBet[],
balanceByUserId: { [userId: string]: number }
) => {
const { newBet } = getBinaryCpmmBetInfo(
outcome,
betAmount ?? 0,
contract,
limitProb,
unfilledBets,
balanceByUserId
)
const remainingMatched =
((newBet.orderAmount ?? 0) - newBet.amount) /
(outcome === 'YES' ? limitProb : 1 - limitProb)
const currentPayout = newBet.shares + remainingMatched
const currentReturn = betAmount ? (currentPayout - betAmount) / betAmount : 0
const totalFees = sum(Object.values(newBet.fees))
return { currentPayout, currentReturn, totalFees, newBet }
}
export const getNewBinaryDpmBetInfo = ( export const getNewBinaryDpmBetInfo = (
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
amount: number, amount: number,
contract: DPMBinaryContract, contract: DPMBinaryContract
loanAmount: number
) => { ) => {
const { YES: yesPool, NO: noPool } = contract.pool const { YES: yesPool, NO: noPool } = contract.pool
@ -274,7 +328,7 @@ export const getNewBinaryDpmBetInfo = (
const newBet: CandidateBet = { const newBet: CandidateBet = {
contractId: contract.id, contractId: contract.id,
amount, amount,
loanAmount, loanAmount: 0,
shares, shares,
outcome, outcome,
probBefore, probBefore,
@ -289,8 +343,7 @@ export const getNewBinaryDpmBetInfo = (
export const getNewMultiBetInfo = ( export const getNewMultiBetInfo = (
outcome: string, outcome: string,
amount: number, amount: number,
contract: FreeResponseContract, contract: DPMContract
loanAmount: number
) => { ) => {
const { pool, totalShares, totalBets } = contract const { pool, totalShares, totalBets } = contract
@ -311,7 +364,7 @@ export const getNewMultiBetInfo = (
const newBet: CandidateBet = { const newBet: CandidateBet = {
contractId: contract.id, contractId: contract.id,
amount, amount,
loanAmount, loanAmount: 0,
shares, shares,
outcome, outcome,
probBefore, probBefore,
@ -365,13 +418,3 @@ export const getNumericBetsInfo = (
return { newBet, newPool, newTotalShares, newTotalBets } return { newBet, newPool, newTotalShares, newTotalBets }
} }
export const getLoanAmount = (yourBets: Bet[], newBetAmount: number) => {
const openBets = yourBets.filter((bet) => !bet.isSold && !bet.sale)
const prevLoanAmount = sumBy(openBets, (bet) => bet.loanAmount ?? 0)
const loanAmount = Math.min(
newBetAmount,
MAX_LOAN_PER_CONTRACT - prevLoanAmount
)
return loanAmount
}

View File

@ -5,12 +5,13 @@ import {
CPMM, CPMM,
DPM, DPM,
FreeResponse, FreeResponse,
MultipleChoice,
Numeric, Numeric,
outcomeType, outcomeType,
PseudoNumeric, PseudoNumeric,
visibility,
} from './contract' } from './contract'
import { User } from './user' import { User } from './user'
import { parseTags, richTextToString } from './util/parse'
import { removeUndefinedProps } from './util/object' import { removeUndefinedProps } from './util/object'
import { JSONContent } from '@tiptap/core' import { JSONContent } from '@tiptap/core'
@ -30,17 +31,12 @@ export function getNewContract(
bucketCount: number, bucketCount: number,
min: number, min: number,
max: number, max: number,
isLogScale: boolean isLogScale: boolean,
) {
const tags = parseTags(
[
question,
richTextToString(description),
...extraTags.map((tag) => `#${tag}`),
].join(' ')
)
const lowercaseTags = tags.map((tag) => tag.toLowerCase())
// for multiple choice
answers: string[],
visibility: visibility
) {
const propsByOutcomeType = const propsByOutcomeType =
outcomeType === 'BINARY' outcomeType === 'BINARY'
? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante) ? getBinaryCpmmProps(initialProb, ante) // getBinaryDpmProps(initialProb, ante)
@ -48,6 +44,8 @@ export function getNewContract(
? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale) ? getPseudoNumericCpmmProps(initialProb, ante, min, max, isLogScale)
: outcomeType === 'NUMERIC' : outcomeType === 'NUMERIC'
? getNumericProps(ante, bucketCount, min, max) ? getNumericProps(ante, bucketCount, min, max)
: outcomeType === 'MULTIPLE_CHOICE'
? getMultipleChoiceProps(ante, answers)
: getFreeAnswerProps(ante) : getFreeAnswerProps(ante)
const contract: Contract = removeUndefinedProps({ const contract: Contract = removeUndefinedProps({
@ -62,9 +60,10 @@ export function getNewContract(
question: question.trim(), question: question.trim(),
description, description,
tags, tags: [],
lowercaseTags, lowercaseTags: [],
visibility: 'public', visibility,
unlistedById: visibility === 'unlisted' ? creator.id : undefined,
isResolved: false, isResolved: false,
createdTime: Date.now(), createdTime: Date.now(),
closeTime, closeTime,
@ -72,6 +71,7 @@ export function getNewContract(
volume: 0, volume: 0,
volume24Hours: 0, volume24Hours: 0,
volume7Days: 0, volume7Days: 0,
elasticity: propsByOutcomeType.mechanism === 'cpmm-1' ? 0.38 : 0.75,
collectedFees: { collectedFees: {
creatorFee: 0, creatorFee: 0,
@ -112,9 +112,12 @@ const getBinaryCpmmProps = (initialProb: number, ante: number) => {
mechanism: 'cpmm-1', mechanism: 'cpmm-1',
outcomeType: 'BINARY', outcomeType: 'BINARY',
totalLiquidity: ante, totalLiquidity: ante,
subsidyPool: 0,
initialProbability: p, initialProbability: p,
p, p,
pool: pool, pool: pool,
prob: initialProb,
probChanges: { day: 0, week: 0, month: 0 },
} }
return system return system
@ -151,6 +154,26 @@ const getFreeAnswerProps = (ante: number) => {
return system return system
} }
const getMultipleChoiceProps = (ante: number, answers: string[]) => {
const numAnswers = answers.length
const betAnte = ante / numAnswers
const betShares = Math.sqrt(ante ** 2 / numAnswers)
const defaultValues = (x: any) =>
Object.fromEntries(range(0, numAnswers).map((k) => [k, x]))
const system: DPM & MultipleChoice = {
mechanism: 'dpm-2',
outcomeType: 'MULTIPLE_CHOICE',
pool: defaultValues(betAnte),
totalShares: defaultValues(betShares),
totalBets: defaultValues(betAnte),
answers: [],
}
return system
}
const getNumericProps = ( const getNumericProps = (
ante: number, ante: number,
bucketCount: number, bucketCount: number,

View File

@ -1,8 +1,10 @@
import { notification_preference } from './user-notification-preferences'
export type Notification = { export type Notification = {
id: string id: string
userId: string userId: string
reasonText?: string reasonText?: string
reason?: notification_reason_types reason?: notification_reason_types | notification_preference
createdTime: number createdTime: number
viewTime?: number viewTime?: number
isSeen: boolean isSeen: boolean
@ -15,6 +17,7 @@ export type Notification = {
sourceUserUsername?: string sourceUserUsername?: string
sourceUserAvatarUrl?: string sourceUserAvatarUrl?: string
sourceText?: string sourceText?: string
data?: { [key: string]: any }
sourceContractTitle?: string sourceContractTitle?: string
sourceContractCreatorUsername?: string sourceContractCreatorUsername?: string
@ -25,6 +28,7 @@ export type Notification = {
isSeenOnHref?: string isSeenOnHref?: string
} }
export type notification_source_types = export type notification_source_types =
| 'contract' | 'contract'
| 'comment' | 'comment'
@ -37,6 +41,12 @@ export type notification_source_types =
| 'group' | 'group'
| 'user' | 'user'
| 'bonus' | 'bonus'
| 'challenge'
| 'betting_streak_bonus'
| 'loan'
| 'like'
| 'tip_and_like'
| 'badge'
export type notification_source_update_types = export type notification_source_update_types =
| 'created' | 'created'
@ -45,22 +55,216 @@ export type notification_source_update_types =
| 'deleted' | 'deleted'
| 'closed' | 'closed'
/* Optional - if possible use a notification_preference */
export type notification_reason_types = export type notification_reason_types =
| 'tagged_user' | 'tagged_user'
| 'on_users_contract'
| 'on_contract_with_users_shares_in'
| 'on_contract_with_users_shares_out'
| 'on_contract_with_users_answer'
| 'on_contract_with_users_comment'
| 'reply_to_users_answer'
| 'reply_to_users_comment'
| 'on_new_follow' | 'on_new_follow'
| 'you_follow_user' | 'contract_from_followed_user'
| 'added_you_to_group'
| 'you_referred_user' | 'you_referred_user'
| 'user_joined_to_bet_on_your_market' | 'user_joined_to_bet_on_your_market'
| 'unique_bettors_on_your_contract' | 'unique_bettors_on_your_contract'
| 'on_group_you_are_member_of'
| 'tip_received' | 'tip_received'
| 'bet_fill' | 'bet_fill'
| 'user_joined_from_your_group_invite' | 'user_joined_from_your_group_invite'
| 'challenge_accepted'
| 'betting_streak_incremented'
| 'loan_income'
| 'liked_and_tipped_your_contract'
| 'comment_on_your_contract'
| 'answer_on_your_contract'
| 'comment_on_contract_you_follow'
| 'answer_on_contract_you_follow'
| 'update_on_contract_you_follow'
| 'resolution_on_contract_you_follow'
| 'comment_on_contract_with_users_shares_in'
| 'answer_on_contract_with_users_shares_in'
| 'update_on_contract_with_users_shares_in'
| 'resolution_on_contract_with_users_shares_in'
| 'comment_on_contract_with_users_answer'
| 'update_on_contract_with_users_answer'
| 'resolution_on_contract_with_users_answer'
| 'answer_on_contract_with_users_answer'
| 'comment_on_contract_with_users_comment'
| 'answer_on_contract_with_users_comment'
| 'update_on_contract_with_users_comment'
| 'resolution_on_contract_with_users_comment'
| 'reply_to_users_answer'
| 'reply_to_users_comment'
| 'your_contract_closed'
| 'subsidized_your_market'
type notification_descriptions = {
[key in notification_preference]: {
simple: string
detailed: string
necessary?: boolean
}
}
export const NOTIFICATION_DESCRIPTIONS: notification_descriptions = {
all_answers_on_my_markets: {
simple: 'Answers on your markets',
detailed: 'Answers on your own markets',
},
all_comments_on_my_markets: {
simple: 'Comments on your markets',
detailed: 'Comments on your own markets',
},
answers_by_followed_users_on_watched_markets: {
simple: 'Only answers by users you follow',
detailed: "Only answers by users you follow on markets you're watching",
},
answers_by_market_creator_on_watched_markets: {
simple: 'Only answers by market creator',
detailed: "Only answers by market creator on markets you're watching",
},
betting_streaks: {
simple: `For prediction streaks`,
detailed: `Bonuses for predictions made over consecutive days (Prediction streaks)})`,
},
comments_by_followed_users_on_watched_markets: {
simple: 'Only comments by users you follow',
detailed:
'Only comments by users that you follow on markets that you watch',
},
contract_from_followed_user: {
simple: 'New markets from users you follow',
detailed: 'New markets from users you follow',
},
limit_order_fills: {
simple: 'Limit order fills',
detailed: 'When your limit order is filled by another user',
},
loan_income: {
simple: 'Automatic loans from your predictions in unresolved markets',
detailed:
'Automatic loans from your predictions that are locked in unresolved markets',
},
market_updates_on_watched_markets: {
simple: 'All creator updates',
detailed: 'All market updates made by the creator',
},
market_updates_on_watched_markets_with_shares_in: {
simple: "Only creator updates on markets that you're invested in",
detailed:
"Only updates made by the creator on markets that you're invested in",
},
on_new_follow: {
simple: 'A user followed you',
detailed: 'A user followed you',
},
onboarding_flow: {
simple: 'Emails to help you get started using Manifold',
detailed: 'Emails to help you learn how to use Manifold',
},
probability_updates_on_watched_markets: {
simple: 'Large changes in probability on markets that you watch',
detailed: 'Large changes in probability on markets that you watch',
},
profit_loss_updates: {
simple: 'Weekly portfolio updates',
detailed: 'Weekly portfolio updates',
},
referral_bonuses: {
simple: 'For referring new users',
detailed: 'Bonuses you receive from referring a new user',
},
resolutions_on_watched_markets: {
simple: 'All market resolutions',
detailed: "All resolutions on markets that you're watching",
},
resolutions_on_watched_markets_with_shares_in: {
simple: "Only market resolutions that you're invested in",
detailed:
"Only resolutions of markets you're watching and that you're invested in",
},
subsidized_your_market: {
simple: 'Your market was subsidized',
detailed: 'When someone subsidizes your market',
},
tagged_user: {
simple: 'A user tagged you',
detailed: 'When another use tags you',
},
thank_you_for_purchases: {
simple: 'Thank you notes for your purchases',
detailed: 'Thank you notes for your purchases',
},
tipped_comments_on_watched_markets: {
simple: 'Only highly tipped comments on markets that you watch',
detailed: 'Only highly tipped comments on markets that you watch',
},
tips_on_your_comments: {
simple: 'Tips on your comments',
detailed: 'Tips on your comments',
},
tips_on_your_markets: {
simple: 'Tips/Likes on your markets',
detailed: 'Tips/Likes on your markets',
},
trending_markets: {
simple: 'Weekly interesting markets',
detailed: 'Weekly interesting markets',
},
unique_bettors_on_your_contract: {
simple: 'For unique predictors on your markets',
detailed: 'Bonuses for unique predictors on your markets',
},
your_contract_closed: {
simple: 'Your market has closed and you need to resolve it (necessary)',
detailed: 'Your market has closed and you need to resolve it (necessary)',
necessary: true,
},
all_comments_on_watched_markets: {
simple: 'All new comments',
detailed: 'All new comments on markets you follow',
},
all_comments_on_contracts_with_shares_in_on_watched_markets: {
simple: `Only on markets you're invested in`,
detailed: `Comments on markets that you're watching and you're invested in`,
},
all_replies_to_my_comments_on_watched_markets: {
simple: 'Only replies to your comments',
detailed: "Only replies to your comments on markets you're watching",
},
all_replies_to_my_answers_on_watched_markets: {
simple: 'Only replies to your answers',
detailed: "Only replies to your answers on markets you're watching",
},
all_answers_on_watched_markets: {
simple: 'All new answers',
detailed: "All new answers on markets you're watching",
},
all_answers_on_contracts_with_shares_in_on_watched_markets: {
simple: `Only on markets you're invested in`,
detailed: `Answers on markets that you're watching and that you're invested in`,
},
badges_awarded: {
simple: 'New badges awarded',
detailed: 'New badges you have earned',
},
opt_out_all: {
simple: 'Opt out of all notifications (excludes when your markets close)',
detailed:
'Opt out of all notifications excluding your own market closure notifications',
},
}
export type BettingStreakData = {
streak: number
bonusAmount: number
}
export type BetFillData = {
betOutcome: string
creatorOutcome: string
probability: number
fillAmount: number
limitOrderTotal?: number
limitOrderRemaining?: number
}
export type ContractResolutionData = {
outcome: string
userPayout: number
userInvestment: number
}

View File

@ -3,4 +3,3 @@ export const NUMERIC_FIXED_VAR = 0.005
export const NUMERIC_GRAPH_COLOR = '#5fa5f9' export const NUMERIC_GRAPH_COLOR = '#5fa5f9'
export const NUMERIC_TEXT_COLOR = 'text-blue-500' export const NUMERIC_TEXT_COLOR = 'text-blue-500'
export const UNIQUE_BETTOR_BONUS_AMOUNT = 5

View File

@ -8,9 +8,13 @@
}, },
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.190", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"lodash": "4.17.21" "lodash": "4.17.21"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,14 +2,17 @@ import { sum, groupBy, sumBy, mapValues } from 'lodash'
import { Bet, NumericBet } from './bet' import { Bet, NumericBet } from './bet'
import { deductDpmFees, getDpmProbability } from './calculate-dpm' import { deductDpmFees, getDpmProbability } from './calculate-dpm'
import { DPMContract, FreeResponseContract } from './contract' import {
DPMContract,
FreeResponseContract,
MultipleChoiceContract,
} from './contract'
import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees' import { DPM_CREATOR_FEE, DPM_FEES, DPM_PLATFORM_FEE } from './fees'
import { addObjects } from './util/object' import { addObjects } from './util/object'
export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => { export const getDpmCancelPayouts = (contract: DPMContract, bets: Bet[]) => {
const { pool } = contract const { pool } = contract
const poolTotal = sum(Object.values(pool)) const poolTotal = sum(Object.values(pool))
console.log('resolved N/A, pool M$', poolTotal)
const betSum = sumBy(bets, (b) => b.amount) const betSum = sumBy(bets, (b) => b.amount)
@ -54,17 +57,6 @@ export const getDpmStandardPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -106,17 +98,6 @@ export const getNumericDpmPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved numeric bucket: ',
outcome,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -159,17 +140,6 @@ export const getDpmMktPayouts = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved MKT',
p,
'pool',
pool,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,
@ -180,7 +150,7 @@ export const getDpmMktPayouts = (
export const getPayoutsMultiOutcome = ( export const getPayoutsMultiOutcome = (
resolutions: { [outcome: string]: number }, resolutions: { [outcome: string]: number },
contract: FreeResponseContract, contract: FreeResponseContract | MultipleChoiceContract,
bets: Bet[] bets: Bet[]
) => { ) => {
const poolTotal = sum(Object.values(contract.pool)) const poolTotal = sum(Object.values(contract.pool))
@ -198,7 +168,7 @@ export const getPayoutsMultiOutcome = (
const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal const winnings = (shares / sharesByOutcome[outcome]) * prob * poolTotal
const profit = winnings - amount const profit = winnings - amount
const payout = amount + (1 - DPM_FEES) * Math.max(0, profit) const payout = amount + (1 - DPM_FEES) * profit
return { userId, profit, payout } return { userId, profit, payout }
}) })
@ -212,16 +182,6 @@ export const getPayoutsMultiOutcome = (
liquidityFee: 0, liquidityFee: 0,
}) })
console.log(
'resolved',
resolutions,
'pool',
poolTotal,
'profits',
profits,
'creator fee',
creatorFee
)
return { return {
payouts: payouts.map(({ userId, payout }) => ({ userId, payout })), payouts: payouts.map(({ userId, payout }) => ({ userId, payout })),
creatorPayout: creatorFee, creatorPayout: creatorFee,

View File

@ -1,5 +1,3 @@
import { sum } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getProbability } from './calculate' import { getProbability } from './calculate'
import { getCpmmLiquidityPoolWeights } from './calculate-cpmm' import { getCpmmLiquidityPoolWeights } from './calculate-cpmm'
@ -43,18 +41,6 @@ export const getStandardFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved',
outcome,
'pool',
contract.pool[outcome],
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolPayouts( const liquidityPayouts = getLiquidityPoolPayouts(
contract, contract,
outcome, outcome,
@ -69,10 +55,11 @@ export const getLiquidityPoolPayouts = (
outcome: string, outcome: string,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[]
) => { ) => {
const { pool } = contract const { pool, subsidyPool } = contract
const finalPool = pool[outcome] const finalPool = pool[outcome] + (subsidyPool ?? 0)
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false) const weights = getCpmmLiquidityPoolWeights(liquidities)
return Object.entries(weights).map(([providerId, weight]) => ({ return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId, userId: providerId,
@ -98,18 +85,6 @@ export const getMktFixedPayouts = (
const { collectedFees } = contract const { collectedFees } = contract
const creatorPayout = collectedFees.creatorFee const creatorPayout = collectedFees.creatorFee
console.log(
'resolved PROB',
p,
'pool',
p * contract.pool.YES + (1 - p) * contract.pool.NO,
'payouts',
sum(payouts),
'creator fee',
creatorPayout
)
const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities) const liquidityPayouts = getLiquidityPoolProbPayouts(contract, p, liquidities)
return { payouts, creatorPayout, liquidityPayouts, collectedFees } return { payouts, creatorPayout, liquidityPayouts, collectedFees }
@ -120,10 +95,11 @@ export const getLiquidityPoolProbPayouts = (
p: number, p: number,
liquidities: LiquidityProvision[] liquidities: LiquidityProvision[]
) => { ) => {
const { pool } = contract const { pool, subsidyPool } = contract
const finalPool = p * pool.YES + (1 - p) * pool.NO const finalPool = p * pool.YES + (1 - p) * pool.NO + (subsidyPool ?? 0)
if (finalPool < 1e-3) return []
const weights = getCpmmLiquidityPoolWeights(contract, liquidities, false) const weights = getCpmmLiquidityPoolWeights(liquidities)
return Object.entries(weights).map(([providerId, weight]) => ({ return Object.entries(weights).map(([providerId, weight]) => ({
userId: providerId, userId: providerId,

View File

@ -117,6 +117,7 @@ export const getDpmPayouts = (
resolutionProbability?: number resolutionProbability?: number
): PayoutInfo => { ): PayoutInfo => {
const openBets = bets.filter((b) => !b.isSold && !b.sale) const openBets = bets.filter((b) => !b.isSold && !b.sale)
const { outcomeType } = contract
switch (outcome) { switch (outcome) {
case 'YES': case 'YES':
@ -124,7 +125,8 @@ export const getDpmPayouts = (
return getDpmStandardPayouts(outcome, contract, openBets) return getDpmStandardPayouts(outcome, contract, openBets)
case 'MKT': case 'MKT':
return contract.outcomeType === 'FREE_RESPONSE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return outcomeType === 'FREE_RESPONSE' ||
outcomeType === 'MULTIPLE_CHOICE' // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
? getPayoutsMultiOutcome(resolutions!, contract, openBets) ? getPayoutsMultiOutcome(resolutions!, contract, openBets)
: getDpmMktPayouts(contract, openBets, resolutionProbability) : getDpmMktPayouts(contract, openBets, resolutionProbability)
case 'CANCEL': case 'CANCEL':
@ -132,7 +134,7 @@ export const getDpmPayouts = (
return getDpmCancelPayouts(contract, openBets) return getDpmCancelPayouts(contract, openBets)
default: default:
if (contract.outcomeType === 'NUMERIC') if (outcomeType === 'NUMERIC')
return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[]) return getNumericDpmPayouts(outcome, contract, openBets as NumericBet[])
// Outcome is a free response answer id. // Outcome is a free response answer id.

29
common/post.ts Normal file
View File

@ -0,0 +1,29 @@
import { JSONContent } from '@tiptap/core'
export type Post = {
id: string
title: string
subtitle: string
content: JSONContent
creatorId: string // User id
createdTime: number
slug: string
// denormalized user fields
creatorName: string
creatorUsername: string
creatorAvatarUrl?: string
likedByUserIds?: string[]
likedByUserCount?: number
}
export type DateDoc = Post & {
bounty: number
birthday: number
type: 'date-doc'
contractSlug: string
}
export const MAX_POST_TITLE_LENGTH = 480
export const MAX_POST_SUBTITLE_LENGTH = 480

View File

@ -37,6 +37,9 @@ export const getPseudoProbability = (
max: number, max: number,
isLogScale = false isLogScale = false
) => { ) => {
if (value < min) return 0
if (value > max) return 1
if (isLogScale) { if (isLogScale) {
return Math.log10(value - min + 1) / Math.log10(max - min + 1) return Math.log10(value - min + 1) / Math.log10(max - min + 1)
} }

View File

@ -1,187 +0,0 @@
import { union, sum, sumBy, sortBy, groupBy, mapValues } from 'lodash'
import { Bet } from './bet'
import { Contract } from './contract'
import { ClickEvent } from './tracking'
import { filterDefined } from './util/array'
import { addObjects } from './util/object'
export const MAX_FEED_CONTRACTS = 75
export const getRecommendedContracts = (
contractsById: { [contractId: string]: Contract },
yourBetOnContractIds: string[]
) => {
const contracts = Object.values(contractsById)
const yourContracts = filterDefined(
yourBetOnContractIds.map((contractId) => contractsById[contractId])
)
const yourContractIds = new Set(yourContracts.map((c) => c.id))
const notYourContracts = contracts.filter((c) => !yourContractIds.has(c.id))
const yourWordFrequency = contractsToWordFrequency(yourContracts)
const otherWordFrequency = contractsToWordFrequency(notYourContracts)
const words = union(
Object.keys(yourWordFrequency),
Object.keys(otherWordFrequency)
)
const yourWeightedFrequency = Object.fromEntries(
words.map((word) => {
const [yourFreq, otherFreq] = [
yourWordFrequency[word] ?? 0,
otherWordFrequency[word] ?? 0,
]
const score = yourFreq / (yourFreq + otherFreq + 0.0001)
return [word, score]
})
)
// console.log(
// 'your weighted frequency',
// _.sortBy(_.toPairs(yourWeightedFrequency), ([, freq]) => -freq)
// )
const scoredContracts = contracts.map((contract) => {
const wordFrequency = contractToWordFrequency(contract)
const score = sumBy(Object.keys(wordFrequency), (word) => {
const wordFreq = wordFrequency[word] ?? 0
const weight = yourWeightedFrequency[word] ?? 0
return wordFreq * weight
})
return {
contract,
score,
}
})
return sortBy(scoredContracts, (scored) => -scored.score).map(
(scored) => scored.contract
)
}
const contractToText = (contract: Contract) => {
const { description, question, tags, creatorUsername } = contract
return `${creatorUsername} ${question} ${tags.join(' ')} ${description}`
}
const MAX_CHARS_IN_WORD = 100
const getWordsCount = (text: string) => {
const normalizedText = text.replace(/[^a-zA-Z]/g, ' ').toLowerCase()
const words = normalizedText
.split(' ')
.filter((word) => word)
.filter((word) => word.length <= MAX_CHARS_IN_WORD)
const counts: { [word: string]: number } = {}
for (const word of words) {
if (counts[word]) counts[word]++
else counts[word] = 1
}
return counts
}
const toFrequency = (counts: { [word: string]: number }) => {
const total = sum(Object.values(counts))
return mapValues(counts, (count) => count / total)
}
const contractToWordFrequency = (contract: Contract) =>
toFrequency(getWordsCount(contractToText(contract)))
const contractsToWordFrequency = (contracts: Contract[]) => {
const frequencySum = contracts
.map(contractToWordFrequency)
.reduce(addObjects, {})
return toFrequency(frequencySum)
}
export const getWordScores = (
contracts: Contract[],
contractViewCounts: { [contractId: string]: number },
clicks: ClickEvent[],
bets: Bet[]
) => {
const contractClicks = groupBy(clicks, (click) => click.contractId)
const contractBets = groupBy(bets, (bet) => bet.contractId)
const yourContracts = contracts.filter(
(c) =>
contractViewCounts[c.id] || contractClicks[c.id] || contractBets[c.id]
)
const yourTfIdf = calculateContractTfIdf(yourContracts)
const contractWordScores = mapValues(yourTfIdf, (wordsTfIdf, contractId) => {
const viewCount = contractViewCounts[contractId] ?? 0
const clickCount = contractClicks[contractId]?.length ?? 0
const betCount = contractBets[contractId]?.length ?? 0
const factor =
-1 * Math.log(viewCount + 1) +
10 * Math.log(betCount + clickCount / 4 + 1)
return mapValues(wordsTfIdf, (tfIdf) => tfIdf * factor)
})
const wordScores = Object.values(contractWordScores).reduce(addObjects, {})
const minScore = Math.min(...Object.values(wordScores))
const maxScore = Math.max(...Object.values(wordScores))
const normalizedWordScores = mapValues(
wordScores,
(score) => (score - minScore) / (maxScore - minScore)
)
// console.log(
// 'your word scores',
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(0, 100),
// _.sortBy(_.toPairs(normalizedWordScores), ([, score]) => -score).slice(-100)
// )
return normalizedWordScores
}
export function getContractScore(
contract: Contract,
wordScores: { [word: string]: number }
) {
if (Object.keys(wordScores).length === 0) return 1
const wordFrequency = contractToWordFrequency(contract)
const score = sumBy(Object.keys(wordFrequency), (word) => {
const wordFreq = wordFrequency[word] ?? 0
const weight = wordScores[word] ?? 0
return wordFreq * weight
})
return score
}
// Caluculate Term Frequency-Inverse Document Frequency (TF-IDF):
// https://medium.datadriveninvestor.com/tf-idf-in-natural-language-processing-8db8ef4a7736
function calculateContractTfIdf(contracts: Contract[]) {
const contractFreq = contracts.map((c) => contractToWordFrequency(c))
const contractWords = contractFreq.map((freq) => Object.keys(freq))
const wordsCount: { [word: string]: number } = {}
for (const words of contractWords) {
for (const word of words) {
wordsCount[word] = (wordsCount[word] ?? 0) + 1
}
}
const wordIdf = mapValues(wordsCount, (count) =>
Math.log(contracts.length / count)
)
const contractWordsTfIdf = contractFreq.map((wordFreq) =>
mapValues(wordFreq, (freq, word) => freq * wordIdf[word])
)
return Object.fromEntries(
contracts.map((c, i) => [c.id, contractWordsTfIdf[i]])
)
}

View File

@ -13,8 +13,12 @@ export const getRedeemableAmount = (bets: RedeemableBet[]) => {
const yesShares = sumBy(yesBets, (b) => b.shares) const yesShares = sumBy(yesBets, (b) => b.shares)
const noShares = sumBy(noBets, (b) => b.shares) const noShares = sumBy(noBets, (b) => b.shares)
const shares = Math.max(Math.min(yesShares, noShares), 0) const shares = Math.max(Math.min(yesShares, noShares), 0)
const soldFrac =
shares > 0
? Math.min(yesShares, noShares) / Math.max(yesShares, noShares)
: 0
const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0) const loanAmount = sumBy(bets, (bet) => bet.loanAmount ?? 0)
const loanPayment = Math.min(loanAmount, shares) const loanPayment = loanAmount * soldFrac
const netAmount = shares - loanPayment const netAmount = shares - loanPayment
return { shares, loanPayment, netAmount } return { shares, loanPayment, netAmount }
} }

View File

@ -1,8 +1,9 @@
import { groupBy, sumBy, mapValues, partition } from 'lodash' import { groupBy, sumBy, mapValues, keyBy, sortBy } from 'lodash'
import { Bet } from './bet' import { Bet } from './bet'
import { getContractBetMetrics, resolvedPayout } from './calculate'
import { Contract } from './contract' import { Contract } from './contract'
import { getPayouts } from './payouts' import { ContractComment } from './comment'
export function scoreCreators(contracts: Contract[]) { export function scoreCreators(contracts: Contract[]) {
const creatorScore = mapValues( const creatorScore = mapValues(
@ -30,46 +31,11 @@ export function scoreTraders(contracts: Contract[], bets: Bet[][]) {
} }
export function scoreUsersByContract(contract: Contract, bets: Bet[]) { export function scoreUsersByContract(contract: Contract, bets: Bet[]) {
const { resolution } = contract const betsByUser = groupBy(bets, (bet) => bet.userId)
const resolutionProb = return mapValues(
contract.outcomeType == 'BINARY' betsByUser,
? contract.resolutionProbability (bets) => getContractBetMetrics(contract, bets).profit
: undefined
const [closedBets, openBets] = partition(
bets,
(bet) => bet.isSold || bet.sale
) )
const { payouts: resolvePayouts } = getPayouts(
resolution as string,
contract,
openBets,
[],
{},
resolutionProb
)
const salePayouts = closedBets.map((bet) => {
const { userId, sale } = bet
return { userId, payout: sale ? sale.amount : 0 }
})
const investments = bets
.filter((bet) => !bet.sale)
.map((bet) => {
const { userId, amount, loanAmount } = bet
const payout = -amount - (loanAmount ?? 0)
return { userId, payout }
})
const netPayouts = [...resolvePayouts, ...salePayouts, ...investments]
const userScore = mapValues(
groupBy(netPayouts, (payout) => payout.userId),
(payouts) => sumBy(payouts, ({ payout }) => payout)
)
return userScore
} }
export function addUserScores( export function addUserScores(
@ -81,3 +47,47 @@ export function addUserScores(
dest[userId] += score dest[userId] += score
} }
} }
export function scoreCommentorsAndBettors(
contract: Contract,
bets: Bet[],
comments: ContractComment[]
) {
const commentsById = keyBy(comments, 'id')
const betsById = keyBy(bets, 'id')
// If 'id2' is the sale of 'id1', both are logged with (id2 - id1) of profit
// Otherwise, we record the profit at resolution time
const profitById: Record<string, number> = {}
for (const bet of bets) {
if (bet.sale) {
const originalBet = betsById[bet.sale.betId]
const profit = bet.sale.amount - originalBet.amount
profitById[bet.id] = profit
profitById[originalBet.id] = profit
} else {
profitById[bet.id] = resolvedPayout(contract, bet) - bet.amount
}
}
// Now find the betId with the highest profit
const topBetId = sortBy(bets, (b) => -profitById[b.id])[0]?.id
const topBettor = betsById[topBetId]?.userName
// And also the commentId of the comment with the highest profit
const topCommentId = sortBy(
comments,
(c) => c.betId && -profitById[c.betId]
)[0]?.id
const topCommentBetId = commentsById[topCommentId]?.betId
return {
topCommentId,
topBetId,
topBettor,
profitById,
commentsById,
betsById,
topCommentBetId,
}
}

View File

@ -9,11 +9,14 @@ import { CPMMContract, DPMContract } from './contract'
import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees' import { DPM_CREATOR_FEE, DPM_PLATFORM_FEE, Fees } from './fees'
import { sumBy } from 'lodash' import { sumBy } from 'lodash'
export type CandidateBet<T extends Bet> = Omit<T, 'id' | 'userId'> export type CandidateBet<T extends Bet> = Omit<
T,
'id' | 'userId' | 'userAvatarUrl' | 'userName' | 'userUsername'
>
export const getSellBetInfo = (bet: Bet, contract: DPMContract) => { export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
const { pool, totalShares, totalBets } = contract const { pool, totalShares, totalBets } = contract
const { id: betId, amount, shares, outcome } = bet const { id: betId, amount, shares, outcome, loanAmount } = bet
const adjShareValue = calculateDpmShareValue(contract, bet) const adjShareValue = calculateDpmShareValue(contract, bet)
@ -64,6 +67,7 @@ export const getSellBetInfo = (bet: Bet, contract: DPMContract) => {
betId, betId,
}, },
fees, fees,
loanAmount: -(loanAmount ?? 0),
} }
return { return {
@ -79,19 +83,20 @@ export const getCpmmSellBetInfo = (
shares: number, shares: number,
outcome: 'YES' | 'NO', outcome: 'YES' | 'NO',
contract: CPMMContract, contract: CPMMContract,
prevLoanAmount: number, unfilledBets: LimitBet[],
unfilledBets: LimitBet[] balanceByUserId: { [userId: string]: number },
loanPaid: number
) => { ) => {
const { pool, p } = contract const { pool, p } = contract
const { saleValue, cpmmState, fees, makers, takers } = calculateCpmmSale( const { saleValue, cpmmState, fees, makers, takers, ordersToCancel } = calculateCpmmSale(
contract, contract,
shares, shares,
outcome, outcome,
unfilledBets unfilledBets,
balanceByUserId,
) )
const loanPaid = Math.min(prevLoanAmount, saleValue)
const probBefore = getCpmmProbability(pool, p) const probBefore = getCpmmProbability(pool, p)
const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p) const probAfter = getCpmmProbability(cpmmState.pool, cpmmState.p)
@ -131,5 +136,6 @@ export const getCpmmSellBetInfo = (
fees, fees,
makers, makers,
takers, takers,
ordersToCancel
} }
} }

View File

@ -1,20 +1,22 @@
export type Stats = { export type Stats = {
startDate: number startDate: number
dailyActiveUsers: number[] dailyActiveUsers: number[]
dailyActiveUsersWeeklyAvg: number[]
weeklyActiveUsers: number[] weeklyActiveUsers: number[]
monthlyActiveUsers: number[] monthlyActiveUsers: number[]
d1: number[]
d1WeeklyAvg: number[]
nd1: number[]
nd1WeeklyAvg: number[]
nw1: number[]
dailyBetCounts: number[] dailyBetCounts: number[]
dailyContractCounts: number[] dailyContractCounts: number[]
dailyCommentCounts: number[] dailyCommentCounts: number[]
dailySignups: number[] dailySignups: number[]
weekOnWeekRetention: number[] weekOnWeekRetention: number[]
monthlyRetention: number[] monthlyRetention: number[]
weeklyActivationRate: number[] dailyActivationRate: number[]
topTenthActions: { dailyActivationRateWeeklyAvg: number[]
daily: number[]
weekly: number[]
monthly: number[]
}
manaBet: { manaBet: {
daily: number[] daily: number[]
weekly: number[] weekly: number[]

View File

@ -1,6 +1,14 @@
// A txn (pronounced "texan") respresents a payment between two ids on Manifold // A txn (pronounced "texan") respresents a payment between two ids on Manifold
// Shortened from "transaction" to distinguish from Firebase transactions (and save chars) // Shortened from "transaction" to distinguish from Firebase transactions (and save chars)
type AnyTxnType = Donation | Tip | Manalink | Referral | Bonus type AnyTxnType =
| Donation
| Tip
| Manalink
| Referral
| UniqueBettorBonus
| BettingStreakBonus
| CancelUniqueBettorBonus
| CommentBountyRefund
type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK' type SourceType = 'USER' | 'CONTRACT' | 'CHARITY' | 'BANK'
export type Txn<T extends AnyTxnType = AnyTxnType> = { export type Txn<T extends AnyTxnType = AnyTxnType> = {
@ -16,7 +24,16 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
amount: number amount: number
token: 'M$' // | 'USD' | MarketOutcome token: 'M$' // | 'USD' | MarketOutcome
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS' category:
| 'CHARITY'
| 'MANALINK'
| 'TIP'
| 'REFERRAL'
| 'UNIQUE_BETTOR_BONUS'
| 'BETTING_STREAK_BONUS'
| 'CANCEL_UNIQUE_BETTOR_BONUS'
| 'COMMENT_BOUNTY'
| 'REFUND_COMMENT_BOUNTY'
// Any extra data // Any extra data
data?: { [key: string]: any } data?: { [key: string]: any }
@ -54,13 +71,70 @@ type Referral = {
category: 'REFERRAL' category: 'REFERRAL'
} }
type Bonus = { type UniqueBettorBonus = {
fromType: 'BANK' fromType: 'BANK'
toType: 'USER' toType: 'USER'
category: 'UNIQUE_BETTOR_BONUS' category: 'UNIQUE_BETTOR_BONUS'
data: {
contractId: string
uniqueNewBettorId?: string
// Old unique bettor bonus txns stored all unique bettor ids
uniqueBettorIds?: string[]
}
}
type BettingStreakBonus = {
fromType: 'BANK'
toType: 'USER'
category: 'BETTING_STREAK_BONUS'
data: {
currentBettingStreak?: number
}
}
type CancelUniqueBettorBonus = {
fromType: 'USER'
toType: 'BANK'
category: 'CANCEL_UNIQUE_BETTOR_BONUS'
data: {
contractId: string
}
}
type CommentBountyDeposit = {
fromType: 'USER'
toType: 'BANK'
category: 'COMMENT_BOUNTY'
data: {
contractId: string
}
}
type CommentBountyWithdrawal = {
fromType: 'BANK'
toType: 'USER'
category: 'COMMENT_BOUNTY'
data: {
contractId: string
commentId: string
}
}
type CommentBountyRefund = {
fromType: 'BANK'
toType: 'USER'
category: 'REFUND_COMMENT_BOUNTY'
data: {
contractId: string
}
} }
export type DonationTxn = Txn & Donation export type DonationTxn = Txn & Donation
export type TipTxn = Txn & Tip export type TipTxn = Txn & Tip
export type ManalinkTxn = Txn & Manalink export type ManalinkTxn = Txn & Manalink
export type ReferralTxn = Txn & Referral export type ReferralTxn = Txn & Referral
export type BettingStreakBonusTxn = Txn & BettingStreakBonus
export type UniqueBettorBonusTxn = Txn & UniqueBettorBonus
export type CancelUniqueBettorBonusTxn = Txn & CancelUniqueBettorBonus
export type CommentBountyDepositTxn = Txn & CommentBountyDeposit
export type CommentBountyWithdrawalTxn = Txn & CommentBountyWithdrawal

View File

@ -0,0 +1,222 @@
import { filterDefined } from './util/array'
import { notification_reason_types } from './notification'
import { getFunctionUrl } from './api'
import { DOMAIN } from './envs/constants'
import { PrivateUser } from './user'
export type notification_destination_types = 'email' | 'browser'
export type notification_preference = keyof notification_preferences
export type notification_preferences = {
// Watched Markets
all_comments_on_watched_markets: notification_destination_types[]
all_answers_on_watched_markets: notification_destination_types[]
// Comments
tipped_comments_on_watched_markets: notification_destination_types[]
comments_by_followed_users_on_watched_markets: notification_destination_types[]
all_replies_to_my_comments_on_watched_markets: notification_destination_types[]
all_replies_to_my_answers_on_watched_markets: notification_destination_types[]
all_comments_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
// Answers
answers_by_followed_users_on_watched_markets: notification_destination_types[]
answers_by_market_creator_on_watched_markets: notification_destination_types[]
all_answers_on_contracts_with_shares_in_on_watched_markets: notification_destination_types[]
// On users' markets
your_contract_closed: notification_destination_types[]
all_comments_on_my_markets: notification_destination_types[]
all_answers_on_my_markets: notification_destination_types[]
subsidized_your_market: notification_destination_types[]
// Market updates
resolutions_on_watched_markets: notification_destination_types[]
resolutions_on_watched_markets_with_shares_in: notification_destination_types[]
market_updates_on_watched_markets: notification_destination_types[]
market_updates_on_watched_markets_with_shares_in: notification_destination_types[]
probability_updates_on_watched_markets: notification_destination_types[]
// Balance Changes
loan_income: notification_destination_types[]
betting_streaks: notification_destination_types[]
referral_bonuses: notification_destination_types[]
unique_bettors_on_your_contract: notification_destination_types[]
tips_on_your_comments: notification_destination_types[]
tips_on_your_markets: notification_destination_types[]
limit_order_fills: notification_destination_types[]
// General
tagged_user: notification_destination_types[]
on_new_follow: notification_destination_types[]
contract_from_followed_user: notification_destination_types[]
trending_markets: notification_destination_types[]
profit_loss_updates: notification_destination_types[]
onboarding_flow: notification_destination_types[]
thank_you_for_purchases: notification_destination_types[]
badges_awarded: notification_destination_types[]
opt_out_all: notification_destination_types[]
// When adding a new notification preference, use add-new-notification-preference.ts to existing users
}
export const getDefaultNotificationPreferences = (
userId: string,
privateUser?: PrivateUser,
noEmails?: boolean
) => {
const constructPref = (browserIf: boolean, emailIf: boolean) => {
const browser = browserIf ? 'browser' : undefined
const email = noEmails ? undefined : emailIf ? 'email' : undefined
return filterDefined([browser, email]) as notification_destination_types[]
}
const defaults: notification_preferences = {
// Watched Markets
all_comments_on_watched_markets: constructPref(true, false),
all_answers_on_watched_markets: constructPref(true, false),
// Comments
tips_on_your_comments: constructPref(true, true),
comments_by_followed_users_on_watched_markets: constructPref(true, true),
all_replies_to_my_comments_on_watched_markets: constructPref(true, true),
all_replies_to_my_answers_on_watched_markets: constructPref(true, true),
all_comments_on_contracts_with_shares_in_on_watched_markets: constructPref(
true,
false
),
// Answers
answers_by_followed_users_on_watched_markets: constructPref(true, true),
answers_by_market_creator_on_watched_markets: constructPref(true, true),
all_answers_on_contracts_with_shares_in_on_watched_markets: constructPref(
true,
true
),
// On users' markets
your_contract_closed: constructPref(true, true), // High priority
all_comments_on_my_markets: constructPref(true, true),
all_answers_on_my_markets: constructPref(true, true),
subsidized_your_market: constructPref(true, true),
// Market updates
resolutions_on_watched_markets: constructPref(true, false),
market_updates_on_watched_markets: constructPref(true, false),
market_updates_on_watched_markets_with_shares_in: constructPref(
true,
false
),
resolutions_on_watched_markets_with_shares_in: constructPref(true, true),
//Balance Changes
loan_income: constructPref(true, false),
betting_streaks: constructPref(true, false),
referral_bonuses: constructPref(true, true),
unique_bettors_on_your_contract: constructPref(true, true),
tipped_comments_on_watched_markets: constructPref(true, true),
tips_on_your_markets: constructPref(true, true),
limit_order_fills: constructPref(true, false),
// General
tagged_user: constructPref(true, true),
on_new_follow: constructPref(true, true),
contract_from_followed_user: constructPref(true, true),
trending_markets: constructPref(false, true),
profit_loss_updates: constructPref(false, true),
probability_updates_on_watched_markets: constructPref(true, false),
thank_you_for_purchases: constructPref(false, false),
onboarding_flow: constructPref(false, false),
opt_out_all: [],
badges_awarded: constructPref(true, false),
}
return defaults
}
// Adding a new key:value here is optional, you can just use a key of notification_subscription_types
// You might want to add a key:value here if there will be multiple notification reasons that map to the same
// subscription type, i.e. 'comment_on_contract_you_follow' and 'comment_on_contract_with_users_answer' both map to
// 'all_comments_on_watched_markets' subscription type
// TODO: perhaps better would be to map notification_subscription_types to arrays of notification_reason_types
const notificationReasonToSubscriptionType: Partial<
Record<notification_reason_types, notification_preference>
> = {
you_referred_user: 'referral_bonuses',
user_joined_to_bet_on_your_market: 'referral_bonuses',
tip_received: 'tips_on_your_comments',
bet_fill: 'limit_order_fills',
user_joined_from_your_group_invite: 'referral_bonuses',
challenge_accepted: 'limit_order_fills',
betting_streak_incremented: 'betting_streaks',
liked_and_tipped_your_contract: 'tips_on_your_markets',
comment_on_your_contract: 'all_comments_on_my_markets',
answer_on_your_contract: 'all_answers_on_my_markets',
comment_on_contract_you_follow: 'all_comments_on_watched_markets',
answer_on_contract_you_follow: 'all_answers_on_watched_markets',
update_on_contract_you_follow: 'market_updates_on_watched_markets',
resolution_on_contract_you_follow: 'resolutions_on_watched_markets',
comment_on_contract_with_users_shares_in:
'all_comments_on_contracts_with_shares_in_on_watched_markets',
answer_on_contract_with_users_shares_in:
'all_answers_on_contracts_with_shares_in_on_watched_markets',
update_on_contract_with_users_shares_in:
'market_updates_on_watched_markets_with_shares_in',
resolution_on_contract_with_users_shares_in:
'resolutions_on_watched_markets_with_shares_in',
comment_on_contract_with_users_answer: 'all_comments_on_watched_markets',
update_on_contract_with_users_answer: 'market_updates_on_watched_markets',
resolution_on_contract_with_users_answer: 'resolutions_on_watched_markets',
answer_on_contract_with_users_answer: 'all_answers_on_watched_markets',
comment_on_contract_with_users_comment: 'all_comments_on_watched_markets',
answer_on_contract_with_users_comment: 'all_answers_on_watched_markets',
update_on_contract_with_users_comment: 'market_updates_on_watched_markets',
resolution_on_contract_with_users_comment: 'resolutions_on_watched_markets',
reply_to_users_answer: 'all_replies_to_my_answers_on_watched_markets',
reply_to_users_comment: 'all_replies_to_my_comments_on_watched_markets',
}
export const getNotificationDestinationsForUser = (
privateUser: PrivateUser,
// TODO: accept reasons array from most to least important and work backwards
reason: notification_reason_types | notification_preference
) => {
const notificationSettings = privateUser.notificationPreferences
const unsubscribeEndpoint = getFunctionUrl('unsubscribe')
try {
let destinations
let subscriptionType: notification_preference | undefined
if (Object.keys(notificationSettings).includes(reason)) {
subscriptionType = reason as notification_preference
destinations = notificationSettings[subscriptionType]
} else {
const key = reason as notification_reason_types
subscriptionType = notificationReasonToSubscriptionType[key]
destinations = subscriptionType
? notificationSettings[subscriptionType]
: []
}
const optOutOfAllSettings = notificationSettings['opt_out_all']
// Your market closure notifications are high priority, opt-out doesn't affect their delivery
const optedOutOfEmail =
optOutOfAllSettings.includes('email') &&
subscriptionType !== 'your_contract_closed'
const optedOutOfBrowser =
optOutOfAllSettings.includes('browser') &&
subscriptionType !== 'your_contract_closed'
return {
sendToEmail: destinations.includes('email') && !optedOutOfEmail,
sendToBrowser: destinations.includes('browser') && !optedOutOfBrowser,
unsubscribeUrl: `${unsubscribeEndpoint}?id=${privateUser.id}&type=${subscriptionType}`,
urlToManageThisNotification: `${DOMAIN}/notifications?tab=settings&section=${subscriptionType}`,
}
} catch (e) {
// Fail safely
console.log(
`couldn't get notification destinations for type ${reason} for user ${privateUser.id}`
)
return {
sendToEmail: false,
sendToBrowser: false,
unsubscribeUrl: '',
urlToManageThisNotification: '',
}
}
}

View File

@ -1,4 +1,6 @@
import { notification_preferences } from './user-notification-preferences'
import { ENV_CONFIG } from './envs/constants' import { ENV_CONFIG } from './envs/constants'
import { MarketCreatorBadge, ProvenCorrectBadge, StreakerBadge } from './badge'
export type User = { export type User = {
id: string id: string
@ -10,7 +12,6 @@ export type User = {
// For their user page // For their user page
bio?: string bio?: string
bannerUrl?: string
website?: string website?: string
twitterHandle?: string twitterHandle?: string
discordHandle?: string discordHandle?: string
@ -32,37 +33,58 @@ export type User = {
allTime: number allTime: number
} }
fractionResolvedCorrectly: number
nextLoanCached: number
followerCountCached: number followerCountCached: number
followedCategories?: string[] followedCategories?: string[]
homeSections?: string[]
referredByUserId?: string referredByUserId?: string
referredByContractId?: string referredByContractId?: string
referredByGroupId?: string referredByGroupId?: string
lastPingTime?: number lastPingTime?: number
shouldShowWelcome?: boolean
lastBetTime?: number
currentBettingStreak?: number
hasSeenContractFollowModal?: boolean
freeMarketsCreated?: number
isBannedFromPosting?: boolean
achievements: {
provenCorrect?: {
badges: ProvenCorrectBadge[]
}
marketCreator?: {
badges: MarketCreatorBadge[]
}
streaker?: {
badges: StreakerBadge[]
}
}
} }
export const STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 1000
// for sus users, i.e. multiple sign ups for same person
export const SUS_STARTING_BALANCE = ENV_CONFIG.startingBalance ?? 10
export const REFERRAL_AMOUNT = ENV_CONFIG.referralBonus ?? 500
export type PrivateUser = { export type PrivateUser = {
id: string // same as User.id id: string // same as User.id
username: string // denormalized from User username: string // denormalized from User
email?: string email?: string
unsubscribedFromResolutionEmails?: boolean weeklyTrendingEmailSent?: boolean
unsubscribedFromCommentEmails?: boolean weeklyPortfolioUpdateEmailSent?: boolean
unsubscribedFromAnswerEmails?: boolean manaBonusEmailSent?: boolean
unsubscribedFromGenericEmails?: boolean
initialDeviceToken?: string initialDeviceToken?: string
initialIpAddress?: string initialIpAddress?: string
apiKey?: string apiKey?: string
notificationPreferences?: notification_subscribe_types notificationPreferences: notification_preferences
twitchInfo?: {
twitchName: string
controlToken: string
botEnabled?: boolean
needsRelinking?: boolean
}
} }
export type notification_subscribe_types = 'all' | 'less' | 'none'
export type PortfolioMetrics = { export type PortfolioMetrics = {
investmentValue: number investmentValue: number
balance: number balance: number
@ -71,5 +93,15 @@ export type PortfolioMetrics = {
userId: string userId: string
} }
export const MANIFOLD_USERNAME = 'ManifoldMarkets' export const MANIFOLD_USER_USERNAME = 'ManifoldMarkets'
export const MANIFOLD_USER_NAME = 'ManifoldMarkets'
export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png' export const MANIFOLD_AVATAR_URL = 'https://manifold.markets/logo-bg-white.png'
// TODO: remove. Hardcoding the strings would be better.
// Different views require different language.
export const BETTOR = ENV_CONFIG.bettor ?? 'trader'
export const BETTORS = ENV_CONFIG.bettor + 's' ?? 'traders'
export const PRESENT_BET = ENV_CONFIG.presentBet ?? 'trade'
export const PRESENT_BETS = ENV_CONFIG.presentBet + 's' ?? 'trades'
export const PAST_BET = ENV_CONFIG.pastBet ?? 'trade'
export const PAST_BETS = ENV_CONFIG.pastBet + 's' ?? 'trades'

View File

@ -1,3 +1,40 @@
import { isEqual } from 'lodash'
export function filterDefined<T>(array: (T | null | undefined)[]) { export function filterDefined<T>(array: (T | null | undefined)[]) {
return array.filter((item) => item !== null && item !== undefined) as T[] return array.filter((item) => item !== null && item !== undefined) as T[]
} }
export function buildArray<T>(
...params: (T | T[] | false | undefined | null)[]
) {
const array: T[] = []
for (const el of params) {
if (Array.isArray(el)) {
array.push(...el)
} else if (el) {
array.push(el)
}
}
return array
}
export function groupConsecutive<T, U>(xs: T[], key: (x: T) => U) {
if (!xs.length) {
return []
}
const result = []
let curr = { key: key(xs[0]), items: [xs[0]] }
for (const x of xs.slice(1)) {
const k = key(x)
if (!isEqual(key, curr.key)) {
result.push(curr)
curr = { key: k, items: [x] }
} else {
curr.items.push(x)
}
}
result.push(curr)
return result
}

24
common/util/color.ts Normal file
View File

@ -0,0 +1,24 @@
export const interpolateColor = (color1: string, color2: string, p: number) => {
const rgb1 = parseInt(color1.replace('#', ''), 16)
const rgb2 = parseInt(color2.replace('#', ''), 16)
const [r1, g1, b1] = toArray(rgb1)
const [r2, g2, b2] = toArray(rgb2)
const q = 1 - p
const rr = Math.round(r1 * q + r2 * p)
const rg = Math.round(g1 * q + g2 * p)
const rb = Math.round(b1 * q + b2 * p)
const hexString = Number((rr << 16) + (rg << 8) + rb).toString(16)
const hex = `#${'0'.repeat(6 - hexString.length)}${hexString}`
return hex
}
function toArray(rgb: number) {
const r = rgb >> 16
const g = (rgb >> 8) % 256
const b = rgb % 256
return [r, g, b]
}

View File

@ -8,7 +8,14 @@ const formatter = new Intl.NumberFormat('en-US', {
}) })
export function formatMoney(amount: number) { export function formatMoney(amount: number) {
const newAmount = Math.round(amount) === 0 ? 0 : Math.floor(amount) // handle -0 case const newAmount =
// handle -0 case
Math.round(amount) === 0
? 0
: // Handle 499.9999999999999 case
(amount > 0 ? Math.floor : Math.ceil)(
amount + 0.00000000001 * Math.sign(amount)
)
return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '') return ENV_CONFIG.moneyMoniker + formatter.format(newAmount).replace('$', '')
} }
@ -53,6 +60,16 @@ export function formatLargeNumber(num: number, sigfigs = 2): string {
return `${numStr}${suffix[i] ?? ''}` return `${numStr}${suffix[i] ?? ''}`
} }
export function shortFormatNumber(num: number): string {
if (num < 1000) return showPrecision(num, 3)
const suffix = ['', 'K', 'M', 'B', 'T', 'Q']
const i = Math.floor(Math.log10(num) / 3)
const numStr = showPrecision(num / Math.pow(10, 3 * i), 2)
return `${numStr}${suffix[i] ?? ''}`
}
export function toCamelCase(words: string) { export function toCamelCase(words: string) {
const camelCase = words const camelCase = words
.split(' ') .split(' ')

View File

@ -1,6 +1,6 @@
import { union } from 'lodash' import { union } from 'lodash'
export const removeUndefinedProps = <T>(obj: T): T => { export const removeUndefinedProps = <T extends object>(obj: T): T => {
const newObj: any = {} const newObj: any = {}
for (const key of Object.keys(obj)) { for (const key of Object.keys(obj)) {
@ -37,4 +37,3 @@ export const subtractObjects = <T extends { [key: string]: number }>(
return newObj as T return newObj as T
} }

View File

@ -1,5 +1,5 @@
import { MAX_TAG_LENGTH } from '../contract' import { generateText, JSONContent, Node } from '@tiptap/core'
import { generateText, JSONContent } from '@tiptap/core' import { generateJSON } from '@tiptap/html'
// Tiptap starter extensions // Tiptap starter extensions
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
@ -20,33 +20,17 @@ import { Text } from '@tiptap/extension-text'
// other tiptap extensions // other tiptap extensions
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { Link } from '@tiptap/extension-link' import { Link } from '@tiptap/extension-link'
import { Mention } from '@tiptap/extension-mention'
import Iframe from './tiptap-iframe'
import TiptapTweet from './tiptap-tweet-type'
import { find } from 'linkifyjs'
import { uniq } from 'lodash'
import { TiptapSpoiler } from './tiptap-spoiler'
export function parseTags(text: string) { /** get first url in text. like "notion.so " -> "http://notion.so"; "notion" -> null */
const regex = /(?:^|\s)(?:[#][a-z0-9_]+)/gi export function getUrl(text: string) {
const matches = (text.match(regex) || []).map((match) => const results = find(text, 'url')
match.trim().substring(1).substring(0, MAX_TAG_LENGTH) return results.length ? results[0].href : null
)
const tagSet = new Set()
const uniqueTags: string[] = []
// Keep casing of last tag.
matches.reverse()
for (const tag of matches) {
const lowercase = tag.toLowerCase()
if (!tagSet.has(lowercase)) {
tagSet.add(lowercase)
uniqueTags.push(tag)
}
}
uniqueTags.reverse()
return uniqueTags
}
export function parseWordsAsTags(text: string) {
const taggedText = text
.split(/\s+/)
.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`))
.join(' ')
return parseTags(taggedText)
} }
// TODO: fuzzy matching // TODO: fuzzy matching
@ -59,8 +43,37 @@ const checkAgainstQuery = (query: string, corpus: string) =>
export const searchInAny = (query: string, ...fields: string[]) => export const searchInAny = (query: string, ...fields: string[]) =>
fields.some((field) => checkAgainstQuery(query, field)) fields.some((field) => checkAgainstQuery(query, field))
// can't just do [StarterKit, Image...] because it doesn't work with cjs imports /** @return user ids of all \@mentions */
export const exhibitExts = [ export function parseMentions(data: JSONContent): string[] {
const mentions = data.content?.flatMap(parseMentions) ?? [] //dfs
if (data.type === 'mention' && data.attrs) {
mentions.push(data.attrs.id as string)
}
return uniq(mentions)
}
// TODO: this is a hack to get around the fact that tiptap doesn't have a
// way to add a node view without bundling in tsx
function skippableComponent(name: string): Node<any, any> {
return Node.create({
name,
group: 'block',
content: 'inline*',
parseHTML() {
return [
{
tag: 'grid-cards-component',
},
]
},
})
}
const stringParseExts = [
// StarterKit extensions
Blockquote, Blockquote,
Bold, Bold,
BulletList, BulletList,
@ -77,12 +90,26 @@ export const exhibitExts = [
Paragraph, Paragraph,
Strike, Strike,
Text, Text,
// other extensions
Image,
Link, Link,
Image.extend({ renderText: () => '[image]' }),
Mention, // user @mention
Mention.extend({ name: 'contract-mention' }), // market %mention
Iframe.extend({
renderText: ({ node }) =>
'[embed]' + node.attrs.src ? `(${node.attrs.src})` : '',
}),
skippableComponent('gridCardsComponent'),
skippableComponent('staticReactEmbedComponent'),
TiptapTweet.extend({ renderText: () => '[tweet]' }),
TiptapSpoiler.extend({ renderHTML: () => ['span', '[spoiler]', 0] }),
] ]
// export const exhibitExts = [StarterKit as unknown as Extension, Image]
export function richTextToString(text?: JSONContent) { export function richTextToString(text?: JSONContent) {
return !text ? '' : generateText(text, exhibitExts) if (!text) return ''
return generateText(text, stringParseExts)
}
export function htmlToRichText(html: string) {
return generateJSON(html, stringParseExts)
} }

View File

@ -46,3 +46,10 @@ export const shuffle = (array: unknown[], rand: () => number) => {
;[array[i], array[swapIndex]] = [array[swapIndex], array[i]] ;[array[i], array[swapIndex]] = [array[swapIndex], array[i]]
} }
} }
export function chooseRandomSubset<T>(items: T[], count: number) {
const fiveMinutes = 5 * 60 * 1000
const seed = Math.round(Date.now() / fiveMinutes).toString()
shuffle(items, createRNG(seed))
return items.slice(0, count)
}

View File

@ -1,2 +1,6 @@
export const HOUR_MS = 60 * 60 * 1000 export const MINUTE_MS = 60 * 1000
export const HOUR_MS = 60 * MINUTE_MS
export const DAY_MS = 24 * HOUR_MS export const DAY_MS = 24 * HOUR_MS
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms))

View File

@ -0,0 +1,100 @@
// Adopted from https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/iframe.ts
import { Node } from '@tiptap/core'
export interface IframeOptions {
allowFullscreen: boolean
HTMLAttributes: {
[key: string]: any
}
}
declare module '@tiptap/core' {
interface Commands<ReturnType> {
iframe: {
setIframe: (options: { src: string }) => ReturnType
}
}
}
// These classes style the outer wrapper and the inner iframe;
// Adopted from css in https://github.com/ueberdosis/tiptap/blob/main/demos/src/Experiments/Embeds/Vue/index.vue
const wrapperClasses = 'relative h-auto w-full overflow-hidden'
const iframeClasses = 'absolute top-0 left-0 h-full w-full'
export default Node.create<IframeOptions>({
name: 'iframe',
group: 'block',
atom: true,
addOptions() {
return {
allowFullscreen: true,
HTMLAttributes: {
class: 'iframe-wrapper' + ' ' + wrapperClasses,
// Tailwind JIT doesn't seem to pick up `pb-[20rem]`, so we hack this in:
style: 'padding-bottom: 20rem; ',
},
}
},
addAttributes() {
return {
src: {
default: null,
},
frameborder: {
default: 0,
},
height: {
default: 0,
},
allowfullscreen: {
default: this.options.allowFullscreen,
parseHTML: () => this.options.allowFullscreen,
},
}
},
parseHTML() {
return [{ tag: 'iframe' }]
},
renderHTML({ HTMLAttributes }) {
this.options.HTMLAttributes.style =
this.options.HTMLAttributes.style +
' height: ' +
HTMLAttributes.height +
';'
return [
'div',
this.options.HTMLAttributes,
[
'iframe',
{
...HTMLAttributes,
class: HTMLAttributes.class + ' ' + iframeClasses,
},
],
]
},
addCommands() {
return {
setIframe:
(options: { src: string }) =>
({ tr, dispatch }) => {
const { selection } = tr
const node = this.type.create(options)
if (dispatch) {
tr.replaceRangeWith(selection.from, selection.to, node)
}
return true
},
}
},
})

View File

@ -0,0 +1,116 @@
// adapted from @n8body/tiptap-spoiler
import {
Mark,
markInputRule,
markPasteRule,
mergeAttributes,
} from '@tiptap/core'
import type { ElementType } from 'react'
declare module '@tiptap/core' {
interface Commands<ReturnType> {
spoilerEditor: {
setSpoiler: () => ReturnType
toggleSpoiler: () => ReturnType
unsetSpoiler: () => ReturnType
}
}
}
export type SpoilerOptions = {
HTMLAttributes: Record<string, any>
spoilerOpenClass: string
spoilerCloseClass?: string
inputRegex: RegExp
pasteRegex: RegExp
as: ElementType
}
const spoilerInputRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))$/
const spoilerPasteRegex = /(?:^|\s)((?:\|\|)((?:[^||]+))(?:\|\|))/g
export const TiptapSpoiler = Mark.create<SpoilerOptions>({
name: 'spoiler',
inline: true,
group: 'inline',
inclusive: false,
exitable: true,
content: 'inline*',
priority: 1001, // higher priority than other formatting so they go inside
addOptions() {
return {
HTMLAttributes: { 'aria-label': 'spoiler' },
spoilerOpenClass: '',
spoilerCloseClass: undefined,
inputRegex: spoilerInputRegex,
pasteRegex: spoilerPasteRegex,
as: 'span',
editing: false,
}
},
addCommands() {
return {
setSpoiler:
() =>
({ commands }) =>
commands.setMark(this.name),
toggleSpoiler:
() =>
({ commands }) =>
commands.toggleMark(this.name),
unsetSpoiler:
() =>
({ commands }) =>
commands.unsetMark(this.name),
}
},
addInputRules() {
return [
markInputRule({
find: this.options.inputRegex,
type: this.type,
}),
]
},
addPasteRules() {
return [
markPasteRule({
find: this.options.pasteRegex,
type: this.type,
}),
]
},
parseHTML() {
return [
{
tag: 'span',
getAttrs: (node) =>
(node as HTMLElement).ariaLabel?.toLowerCase() === 'spoiler' && null,
},
]
},
renderHTML({ HTMLAttributes }) {
const elem = document.createElement(this.options.as as string)
Object.entries(
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
class: this.options.spoilerCloseClass ?? this.options.spoilerOpenClass,
})
).forEach(([attr, val]) => elem.setAttribute(attr, val))
elem.addEventListener('click', () => {
elem.setAttribute('class', this.options.spoilerOpenClass)
})
return elem
},
})

View File

@ -0,0 +1,37 @@
import { Node, mergeAttributes } from '@tiptap/core'
export interface TweetOptions {
tweetId: string
}
// This is a version of the Tiptap Node config without addNodeView,
// since that would require bundling in tsx
export const TiptapTweetNode = {
name: 'tiptapTweet',
group: 'block',
atom: true,
addAttributes() {
return {
tweetId: {
default: null,
},
}
},
parseHTML() {
return [
{
tag: 'tiptap-tweet',
},
]
},
renderHTML(props: { HTMLAttributes: Record<string, any> }) {
return ['tiptap-tweet', mergeAttributes(props.HTMLAttributes)]
},
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export default Node.create<TweetOptions>(TiptapTweetNode)

43
dev.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash
ENV=${1:-dev}
case $ENV in
dev)
FIREBASE_PROJECT=dev
NEXT_ENV=DEV ;;
prod)
FIREBASE_PROJECT=prod
NEXT_ENV=PROD ;;
localdb)
FIREBASE_PROJECT=dev
NEXT_ENV=DEV
EMULATOR=true ;;
*)
echo "Invalid environment; must be dev, prod, or localdb."
exit 1
esac
firebase use $FIREBASE_PROJECT
if [ ! -z $EMULATOR ]
then
npx concurrently \
-n FIRESTORE,FUNCTIONS,NEXT,TS \
-c green,white,magenta,cyan \
"yarn --cwd=functions localDbScript" \
"cross-env FIRESTORE_EMULATOR_HOST=localhost:8080 yarn --cwd=functions dev" \
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
NEXT_PUBLIC_FIREBASE_EMULATE=TRUE \
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
yarn --cwd=web serve" \
"cross-env yarn --cwd=web ts-watch"
else
npx concurrently \
-n FUNCTIONS,NEXT,TS \
-c white,magenta,cyan \
"yarn --cwd=functions dev" \
"cross-env NEXT_PUBLIC_FUNCTIONS_URL=http://localhost:8088 \
NEXT_PUBLIC_FIREBASE_ENV=${NEXT_ENV} \
yarn --cwd=web serve" \
"cross-env yarn --cwd=web ts-watch"
fi

View File

@ -46,6 +46,42 @@ Gets a user by their unique ID. Many other API endpoints return this as the `use
Requires no authorization. Requires no authorization.
### GET /v0/me
Returns the authenticated user.
### `GET /v0/groups`
Gets all groups, in no particular order.
Parameters:
- `availableToUserId`: Optional. if specified, only groups that the user can
join and groups they've already joined will be returned.
Requires no authorization.
### `GET /v0/group/[slug]`
Gets a group by its slug.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]`
Gets a group by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/group/by-id/[id]/markets`
Gets a group's markets by its unique ID.
Requires no authorization.
Note: group is singular in the URL.
### `GET /v0/markets` ### `GET /v0/markets`
Lists all markets, ordered by creation date descending. Lists all markets, ordered by creation date descending.
@ -75,7 +111,6 @@ Requires no authorization.
"creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c", "creatorAvatarUrl":"https://lh3.googleusercontent.com/a-/AOh14GiZyl1lBehuBMGyJYJhZd-N-mstaUtgE4xdI22lLw=s96-c",
"closeTime":1653893940000, "closeTime":1653893940000,
"question":"Will I write a new blog post today?", "question":"Will I write a new blog post today?",
"description":"I'm supposed to, or else Beeminder charges me $90.\nTentative topic ideas:\n- \"Manifold funding, a history\"\n- \"Markets and bounties allow trades through time\"\n- \"equity vs money vs time\"\n\nClose date updated to 2022-05-29 11:59 pm",
"tags":[ "tags":[
"personal", "personal",
"commitments" "commitments"
@ -113,7 +148,6 @@ Requires no authorization.
// Market attributes. All times are in milliseconds since epoch // Market attributes. All times are in milliseconds since epoch
closeTime?: number // Min of creator's chosen date, and resolutionTime closeTime?: number // Min of creator's chosen date, and resolutionTime
question: string question: string
description: string
// A list of tags on each market. Any user can add tags to any market. // A list of tags on each market. Any user can add tags to any market.
// This list also includes the predefined categories shown as filters on the home page. // This list also includes the predefined categories shown as filters on the home page.
@ -124,13 +158,16 @@ Requires no authorization.
// i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market // i.e. https://manifold.markets/Austin/test-market is the same as https://manifold.markets/foo/test-market
url: string url: string
outcomeType: string // BINARY, FREE_RESPONSE, or NUMERIC outcomeType: string // BINARY, FREE_RESPONSE, MULTIPLE_CHOICE, NUMERIC, or PSEUDO_NUMERIC
mechanism: string // dpm-2 or cpmm-1 mechanism: string // dpm-2 or cpmm-1
probability: number probability: number
pool: { outcome: number } // For CPMM markets, the number of shares in the liquidity pool. For DPM markets, the amount of mana invested in each answer. pool: { outcome: number } // For CPMM markets, the number of shares in the liquidity pool. For DPM markets, the amount of mana invested in each answer.
p?: number // CPMM markets only, probability constant in y^p * n^(1-p) = k p?: number // CPMM markets only, probability constant in y^p * n^(1-p) = k
totalLiquidity?: number // CPMM markets only, the amount of mana deposited into the liquidity pool totalLiquidity?: number // CPMM markets only, the amount of mana deposited into the liquidity pool
min?: number // PSEUDO_NUMERIC markets only, the minimum resolvable value
max?: number // PSEUDO_NUMERIC markets only, the maximum resolvable value
isLogScale?: bool // PSEUDO_NUMERIC markets only, if true `number = (max - min + 1)^probability + minstart - 1`, otherwise `number = min + (max - min) * probability`
volume: number volume: number
volume7Days: number volume7Days: number
@ -140,6 +177,8 @@ Requires no authorization.
resolutionTime?: number resolutionTime?: number
resolution?: string resolution?: string
resolutionProbability?: number // Used for BINARY markets resolved to MKT resolutionProbability?: number // Used for BINARY markets resolved to MKT
lastUpdatedTime?: number
} }
``` ```
@ -372,7 +411,9 @@ Requires no authorization.
type FullMarket = LiteMarket & { type FullMarket = LiteMarket & {
bets: Bet[] bets: Bet[]
comments: Comment[] comments: Comment[]
answers?: Answer[] answers?: Answer[] // dpm-2 markets only
description: JSONContent // Rich text content. See https://tiptap.dev/guide/output#option-1-json
textDescription: string // string description without formatting, images, or embeds
} }
type Bet = { type Bet = {
@ -481,6 +522,20 @@ Parameters:
answer. For numeric markets, this is a string representing the target bucket, answer. For numeric markets, this is a string representing the target bucket,
and an additional `value` parameter is required which is a number representing and an additional `value` parameter is required which is a number representing
the target value. (Bet on numeric markets at your own peril.) the target value. (Bet on numeric markets at your own peril.)
- `limitProb`: Optional. A number between `0.001` and `0.999` inclusive representing
the limit probability for your bet (i.e. 0.1% to 99.9% — multiply by 100 for the
probability percentage).
The bet will execute immediately in the direction of `outcome`, but not beyond this
specified limit. If not all the bet is filled, the bet will remain as an open offer
that can later be matched against an opposite direction bet.
- For example, if the current market probability is `50%`:
- A `M$10` bet on `YES` with `limitProb=0.4` would not be filled until the market
probability moves down to `40%` and someone bets `M$15` of `NO` to match your
bet odds.
- A `M$100` bet on `YES` with `limitProb=0.6` would fill partially or completely
depending on current unfilled limit bets and the AMM's liquidity. Any remaining
portion of the bet not filled would remain to be matched against in the future.
- An unfilled limit order bet can be cancelled using the cancel API.
Example request: Example request:
@ -492,15 +547,20 @@ $ curl https://manifold.markets/api/v0/bet -X POST -H 'Content-Type: application
"contractId":"{...}"}' "contractId":"{...}"}'
``` ```
### `POST /v0/bet/cancel/[id]`
Cancel the limit order of a bet with the specified id. If the bet was unfilled, it will be cancelled so that no other bets will match with it. This is action irreversable.
### `POST /v0/market` ### `POST /v0/market`
Creates a new market on behalf of the authorized user. Creates a new market on behalf of the authorized user.
Parameters: Parameters:
- `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, or `NUMERIC`. - `outcomeType`: Required. One of `BINARY`, `FREE_RESPONSE`, `MULTIPLE_CHOICE`, or `PSEUDO_NUMERIC`.
- `question`: Required. The headline question for the market. - `question`: Required. The headline question for the market.
- `description`: Required. A long description describing the rules for the market. - `description`: Required. A long description describing the rules for the market.
- Note: string descriptions do **not** turn into links, mentions, formatted text. Instead, rich text descriptions must be in [TipTap json](https://tiptap.dev/guide/output#option-1-json).
- `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch. - `closeTime`: Required. The time at which the market will close, represented as milliseconds since the epoch.
- `tags`: Optional. An array of string tags for the market. - `tags`: Optional. An array of string tags for the market.
@ -512,6 +572,12 @@ For numeric markets, you must also provide:
- `min`: The minimum value that the market may resolve to. - `min`: The minimum value that the market may resolve to.
- `max`: The maximum value that the market may resolve to. - `max`: The maximum value that the market may resolve to.
- `isLogScale`: If true, your numeric market will increase exponentially from min to max.
- `initialValue`: An initial value for the market, between min and max, exclusive.
For multiple choice markets, you must also provide:
- `answers`: An array of strings, each of which will be a valid answer for the market.
Example request: Example request:
@ -525,6 +591,18 @@ $ curl https://manifold.markets/api/v0/market -X POST -H 'Content-Type: applicat
"initialProb":25}' "initialProb":25}'
``` ```
### `POST /v0/market/[marketId]/add-liquidity`
Adds a specified amount of liquidity into the market.
- `amount`: Required. The amount of liquidity to add, in M$.
### `POST /v0/market/[marketId]/close`
Closes a market on behalf of the authorized user.
- `closeTime`: Optional. Milliseconds since the epoch to close the market at. If not provided, the market will be closed immediately. Cannot provide close time in past.
### `POST /v0/market/[marketId]/resolve` ### `POST /v0/market/[marketId]/resolve`
Resolves a market on behalf of the authorized user. Resolves a market on behalf of the authorized user.
@ -536,15 +614,18 @@ For binary markets:
- `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`. - `outcome`: Required. One of `YES`, `NO`, `MKT`, or `CANCEL`.
- `probabilityInt`: Optional. The probability to use for `MKT` resolution. - `probabilityInt`: Optional. The probability to use for `MKT` resolution.
For free response markets: For free response or multiple choice markets:
- `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the answer index. - `outcome`: Required. One of `MKT`, `CANCEL`, or a `number` indicating the answer index.
- `resolutions`: An array of `{ answer, pct }` objects to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. - `resolutions`: An array of `{ answer, pct }` objects to use as the weights for resolving in favor of multiple free response options. Can only be set with `MKT` outcome. Note that the total weights must add to 100.
For numeric markets: For numeric markets:
- `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID. - `outcome`: Required. One of `CANCEL`, or a `number` indicating the selected numeric bucket ID.
- `value`: The value that the market may resolves to. - `value`: The value that the market may resolves to.
- `probabilityInt`: Required if `value` is present. Should be equal to
- If log scale: `log10(value - min + 1) / log10(max - min + 1)`
- Otherwise: `(value - min) / (max - min)`
Example request: Example request:
@ -579,6 +660,37 @@ $ curl https://manifold.markets/api/v0/market/{marketId}/resolve -X POST \
]}' ]}'
``` ```
### `POST /v0/market/[marketId]/sell`
Sells some quantity of shares in a binary market on behalf of the authorized user.
Parameters:
- `outcome`: Optional. One of `YES`, or `NO`. If you leave it off, and you only
own one kind of shares, you will sell that kind of shares.
- `shares`: Optional. The amount of shares to sell of the outcome given
above. If not provided, all the shares you own will be sold.
Example request:
```
$ curl https://manifold.markets/api/v0/market/{marketId}/sell -X POST \
-H 'Content-Type: application/json' \
-H 'Authorization: Key {...}' \
--data-raw '{"outcome": "YES", "shares": 10}'
```
### `POST /v0/comment`
Creates a comment in the specified market. Only supports top-level comments for now.
Parameters:
- `contractId`: Required. The ID of the market to comment on.
- `content`: The comment to post, formatted as [TipTap json](https://tiptap.dev/guide/output#option-1-json), OR
- `html`: The comment to post, formatted as an HTML string, OR
- `markdown`: The comment to post, formatted as a markdown string.
### `GET /v0/bets` ### `GET /v0/bets`
Gets a list of bets, ordered by creation date descending. Gets a list of bets, ordered by creation date descending.
@ -597,7 +709,7 @@ Requires no authorization.
- Example request - Example request
``` ```
https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-california-abolish-daylight-sa https://manifold.markets/api/v0/bets?username=ManifoldMarkets&market=will-i-be-able-to-place-a-limit-ord
``` ```
- Response type: A `Bet[]`. - Response type: A `Bet[]`.
@ -605,31 +717,60 @@ Requires no authorization.
```json ```json
[ [
// Limit bet, partially filled.
{ {
"probAfter": 0.44418877319153904, "isFilled": false,
"shares": -645.8346334931828, "amount": 15.596681605353808,
"userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"contractId": "Tz5dA01GkK5QKiQfZeDL",
"probBefore": 0.5730753474948571,
"isCancelled": false,
"outcome": "YES", "outcome": "YES",
"contractId": "tgB1XmvFXZNhjr3xMNLp", "fees": { "creatorFee": 0, "liquidityFee": 0, "platformFee": 0 },
"sale": { "shares": 31.193363210707616,
"betId": "RcOtarI3d1DUUTjiE0rx", "limitProb": 0.5,
"amount": 474.9999999999998 "id": "yXB8lVbs86TKkhWA1FVi",
}, "loanAmount": 0,
"createdTime": 1644602886293, "orderAmount": 100,
"userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2", "probAfter": 0.5730753474948571,
"probBefore": 0.7229189477449224, "createdTime": 1659482775970,
"id": "x9eNmCaqQeXW8AgJ8Zmp", "fills": [
"amount": -499.9999999999998 {
"timestamp": 1659483249648,
"matchedBetId": "MfrMd5HTiGASDXzqibr7",
"amount": 15.596681605353808,
"shares": 31.193363210707616
}
]
}, },
// Normal bet (no limitProb specified).
{ {
"probAfter": 0.9901970375647697, "shares": 17.350459904608414,
"contractId": "zdeaYVAfHlo9jKzWh57J", "probBefore": 0.5304358279113885,
"outcome": "YES", "isFilled": true,
"amount": 1, "probAfter": 0.5730753474948571,
"id": "8PqxKYwXCcLYoXy2m2Nm", "userId": "IPTOzEqrpkWmEzh6hwvAyY9PqFb2",
"shares": 1.0049875638533763, "amount": 10,
"userId": "94YYTk1AFWfbWMpfYcvnnwI1veP2", "contractId": "Tz5dA01GkK5QKiQfZeDL",
"probBefore": 0.9900000000000001, "id": "1LPJHNz5oAX4K6YtJlP1",
"createdTime": 1644705818872 "fees": {
"platformFee": 0,
"liquidityFee": 0,
"creatorFee": 0.4251333951457593
},
"isCancelled": false,
"loanAmount": 0,
"orderAmount": 10,
"fills": [
{
"amount": 10,
"matchedBetId": null,
"shares": 17.350459904608414,
"timestamp": 1659482757271
}
],
"createdTime": 1659482757271,
"outcome": "YES"
} }
] ]
``` ```
@ -639,6 +780,7 @@ Requires no authorization.
## Changelog ## Changelog
- 2022-09-24: Expand market POST docs to include new market types (`PSEUDO_NUMERIC`, `MULTIPLE_CHOICE`)
- 2022-07-15: Add user by username and user by ID APIs - 2022-07-15: Add user by username and user by ID APIs
- 2022-06-08: Add paging to markets endpoint - 2022-06-08: Add paging to markets endpoint
- 2022-06-05: Add new authorized write endpoints - 2022-06-05: Add new authorized write endpoints

View File

@ -8,15 +8,40 @@ A list of community-created projects built on, or related to, Manifold Markets.
## Sites using Manifold ## Sites using Manifold
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government - [WagerWith.me](https://www.wagerwith.me/) — Bet with your friends, with full Manifold integration to bet with M$.
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold - [Alignment Markets](https://alignmentmarkets.com/) - Bet on the progress of benchmarks in ML safety!
## API / Dev ## API / Dev
- [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API - [PyManifold](https://github.com/bcongdon/PyManifold) - Python client for the Manifold API
- [PyManifold fork](https://github.com/gappleto97/PyManifold/) - Fork maintained by [@LivInTheLookingGlass](https://manifold.markets/LivInTheLookingGlass)
- [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics) - [manifold-markets-python](https://github.com/vluzko/manifold-markets-python) - Python tools for working with Manifold Markets (including various accuracy metrics)
- [ManifoldMarketManager](https://github.com/gappleto97/ManifoldMarketManager) - Python script and library to automatically manage markets
- [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets - [manifeed](https://github.com/joy-void-joy/manifeed) - Tool that creates an RSS feed for new Manifold markets
- [manifold-sdk](https://github.com/keriwarr/manifold-sdk) - TypeScript/JavaScript client for the Manifold API
## Bots ## Bots
- [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon - [@manifold@botsin.space](https://botsin.space/@manifold) - Posts new Manifold markets to Mastodon
- [James' Bot](https://github.com/manifoldmarkets/market-maker) — Simple trading bot that makes markets
- [mana](https://github.com/AnnikaCodes/mana) - A Discord bot for Manifold by [@arae](https://manifold.markets/arae)
## Writeups
- [Information Markets, Decision Markets, Attention Markets, Action Markets](https://astralcodexten.substack.com/p/information-markets-decision-markets) by Scott Alexander
- [Mismatched Monetary Motivation in Manifold Markets](https://kevin.zielnicki.com/2022/02/17/manifold/) by Kevin Zielnicki
- [Introducing the Salem/CSPI Forecasting Tournament](https://www.cspicenter.com/p/introducing-the-salemcspi-forecasting) by Richard Hanania
- [What I learned about running a betting market game night contest](https://shakeddown.wordpress.com/2022/08/04/what-i-learned-about-running-a-betting-market-game-night-contest/) by shakeddown
- [Free-riding on prediction markets](https://pedunculate.substack.com/p/free-riding-on-prediction-markets) by John Roxton
## Art
- Folded origami and doodles by [@hamnox](https://manifold.markets/hamnox) ![](https://i.imgur.com/nVGY4pL.png)
- Laser-cut Foldy by [@wasabipesto](https://manifold.markets/wasabipesto) ![](https://i.imgur.com/g9S6v3P.jpg)
## Alumni
_These projects are no longer active, but were really really cool!_
- [Research.Bet](https://research.bet/) - Prediction market for scientific papers, using Manifold
- [CivicDashboard](https://civicdash.org/dashboard) - Uses Manifold to for tracked solutions for the SF city government

View File

@ -15,6 +15,22 @@ Our community is the beating heart of Manifold; your individual contributions ar
## Awarded bounties ## Awarded bounties
💥 *Awarded on 2022-10-07*
**[Pepe](https://manifold.markets/Pepe): M$10,000**
**[Jack](https://manifold.markets/jack): M$2,000**
**[Martin](https://manifold.markets/MartinRandall): M$2,000**
**[Yev](https://manifold.markets/Yev): M$2,000**
**[Michael](https://manifold.markets/MichaelWheatley): M$2,000**
- For discovering an infinite mana exploit using limit orders, and informing the Manifold team of it privately.
**[Matt](https://manifold.markets/MattP): M$5,000**
**[Adrian](https://manifold.markets/ahalekelly): M$5,000**
**[Yev](https://manifold.markets/Yev): M$5,000**
- For discovering an AMM liquidity exploit and informing the Manifold team of it privately.
🎈 *Awarded on 2022-06-14* 🎈 *Awarded on 2022-06-14*
**[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000** **[Wasabipesto](https://manifold.markets/wasabipesto): M$20,000**

View File

@ -4,11 +4,7 @@
### Do I have to pay real money in order to participate? ### Do I have to pay real money in order to participate?
Nope! Each account starts with a free M$1000. If you invest it wisely, you can increase your total without ever needing to put any real money into the site. Nope! Each account starts with a free 1000 mana (or M$1000 for short). If you invest it wisely, you can increase your total without ever needing to put any real money into the site.
### What is the name for the currency Manifold uses, represented by M$?
Manifold Dollars, or mana for short.
### Can M$ be sold for real money? ### Can M$ be sold for real money?

View File

@ -30,7 +30,8 @@
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.0.0-beta.17", "@docusaurus/module-type-aliases": "2.0.0-beta.17",
"@tsconfig/docusaurus": "^1.0.4" "@tsconfig/docusaurus": "^1.0.4",
"@types/react": "^17.0.2"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View File

@ -2,10 +2,30 @@
"functions": { "functions": {
"predeploy": "cd functions && yarn build", "predeploy": "cd functions && yarn build",
"runtime": "nodejs16", "runtime": "nodejs16",
"source": "functions/dist" "source": "functions/dist",
"ignore": [
"node_modules",
".git",
"firebase-debug.log",
"firebase-debug.*.log"
]
}, },
"firestore": { "firestore": {
"rules": "firestore.rules", "rules": "firestore.rules",
"indexes": "firestore.indexes.json" "indexes": "firestore.indexes.json"
},
"emulators": {
"functions": {
"port": 5001
},
"firestore": {
"port": 8080
},
"pubsub": {
"port": 8085
},
"ui": {
"enabled": true
}
} }
} }

View File

@ -22,6 +22,20 @@
} }
] ]
}, },
{
"collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "bets", "collectionGroup": "bets",
"queryScope": "COLLECTION_GROUP", "queryScope": "COLLECTION_GROUP",
@ -36,6 +50,84 @@
} }
] ]
}, },
{
"collectionGroup": "bets",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isCancelled",
"order": "ASCENDING"
},
{
"fieldPath": "isFilled",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "challenges",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "commentType",
"order": "ASCENDING"
},
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdTime",
"order": "ASCENDING"
}
]
},
{ {
"collectionGroup": "comments", "collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP", "queryScope": "COLLECTION_GROUP",
@ -78,6 +170,42 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "creatorId",
"order": "ASCENDING"
},
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "groupSlugs",
"arrayConfig": "CONTAINS"
},
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -124,6 +252,46 @@
} }
] ]
}, },
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "closeTime",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "contracts",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "isResolved",
"order": "ASCENDING"
},
{
"fieldPath": "visibility",
"order": "ASCENDING"
},
{
"fieldPath": "popularityScore",
"order": "DESCENDING"
}
]
},
{ {
"collectionGroup": "contracts", "collectionGroup": "contracts",
"queryScope": "COLLECTION", "queryScope": "COLLECTION",
@ -496,6 +664,28 @@
} }
] ]
}, },
{
"collectionGroup": "comments",
"fieldPath": "contractId",
"indexes": [
{
"order": "ASCENDING",
"queryScope": "COLLECTION"
},
{
"order": "DESCENDING",
"queryScope": "COLLECTION"
},
{
"arrayConfig": "CONTAINS",
"queryScope": "COLLECTION"
},
{
"order": "ASCENDING",
"queryScope": "COLLECTION_GROUP"
}
]
},
{ {
"collectionGroup": "comments", "collectionGroup": "comments",
"fieldPath": "createdTime", "fieldPath": "createdTime",

View File

@ -10,7 +10,12 @@ service cloud.firestore {
'akrolsmir@gmail.com', 'akrolsmir@gmail.com',
'jahooma@gmail.com', 'jahooma@gmail.com',
'taowell@gmail.com', 'taowell@gmail.com',
'manticmarkets@gmail.com' 'abc.sinclair@gmail.com',
'manticmarkets@gmail.com',
'iansphilips@gmail.com',
'd4vidchee@gmail.com',
'federicoruizcassarino@gmail.com',
'ingawei@gmail.com'
] ]
} }
@ -18,19 +23,25 @@ service cloud.firestore {
allow read; allow read;
} }
match /globalConfig/globalConfig {
allow read;
allow update: if isAdmin()
allow create: if isAdmin()
}
match /users/{userId} { match /users/{userId} {
allow read; allow read;
allow update: if resource.data.id == request.auth.uid allow update: if userId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['bio', 'bannerUrl', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime']); .hasOnly(['bio', 'website', 'twitterHandle', 'discordHandle', 'followedCategories', 'lastPingTime','shouldShowWelcome', 'hasSeenContractFollowModal', 'homeSections']);
// User referral rules // User referral rules
allow update: if resource.data.id == request.auth.uid allow update: if userId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId']) .hasOnly(['referredByUserId', 'referredByContractId', 'referredByGroupId'])
// only one referral allowed per user // only one referral allowed per user
&& !("referredByUserId" in resource.data) && !("referredByUserId" in resource.data)
// user can't refer themselves // user can't refer themselves
&& !(resource.data.id == request.resource.data.referredByUserId); && !(userId == request.resource.data.referredByUserId);
// quid pro quos enabled (only once though so nbd) - bc I can't make this work: // quid pro quos enabled (only once though so nbd) - bc I can't make this work:
// && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id); // && (get(/databases/$(database)/documents/users/$(request.resource.data.referredByUserId)).referredByUserId == resource.data.id);
} }
@ -39,20 +50,45 @@ service cloud.firestore {
allow read; allow read;
} }
match /{somePath=**}/contract-metrics/{contractId} {
allow read;
}
match /{somePath=**}/challenges/{challengeId}{
allow read;
}
match /contracts/{contractId}/follows/{userId} {
allow read;
allow create, delete: if userId == request.auth.uid;
}
match /contracts/{contractId}/challenges/{challengeId}{
allow read;
allow create: if request.auth.uid == request.resource.data.creatorId;
// allow update if there have been no claims yet and if the challenge is still open
allow update: if request.auth.uid == resource.data.creatorId;
}
match /users/{userId}/follows/{followUserId} { match /users/{userId}/follows/{followUserId} {
allow read; allow read;
allow write: if request.auth.uid == userId; allow write: if request.auth.uid == userId;
} }
match /users/{userId}/likes/{likeId} {
allow read;
allow write: if request.auth.uid == userId;
}
match /{somePath=**}/follows/{followUserId} { match /{somePath=**}/follows/{followUserId} {
allow read; allow read;
} }
match /private-users/{userId} { match /private-users/{userId} {
allow read: if resource.data.id == request.auth.uid || isAdmin(); allow read: if userId == request.auth.uid || isAdmin();
allow update: if (resource.data.id == request.auth.uid || isAdmin()) allow update: if (userId == request.auth.uid || isAdmin())
&& request.resource.data.diff(resource.data).affectedKeys() && request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['apiKey', 'unsubscribedFromResolutionEmails', 'unsubscribedFromCommentEmails', 'unsubscribedFromAnswerEmails', 'notificationPreferences' ]); .hasOnly(['apiKey', 'notificationPreferences', 'twitchInfo']);
} }
match /private-users/{userId}/views/{viewId} { match /private-users/{userId}/views/{viewId} {
@ -74,9 +110,9 @@ service cloud.firestore {
match /contracts/{contractId} { match /contracts/{contractId} {
allow read; allow read;
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['tags', 'lowercaseTags', 'groupSlugs']); .hasOnly(['tags', 'lowercaseTags', 'groupSlugs', 'groupLinks', 'flaggedByUsernames']);
allow update: if request.resource.data.diff(resource.data).affectedKeys() allow update: if request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['description', 'closeTime', 'question']) .hasOnly(['description', 'closeTime', 'question', 'visibility', 'unlistedById'])
&& resource.data.creatorId == request.auth.uid; && resource.data.creatorId == request.auth.uid;
allow update: if isAdmin(); allow update: if isAdmin();
match /comments/{commentId} { match /comments/{commentId} {
@ -137,25 +173,52 @@ service cloud.firestore {
.hasOnly(['isSeen', 'viewTime']); .hasOnly(['isSeen', 'viewTime']);
} }
match /groups/{groupId} { match /{somePath=**}/groupMembers/{memberId} {
allow read; allow read;
allow update: if request.auth.uid == resource.data.creatorId }
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly(['name', 'about', 'contractIds', 'memberIds', 'anyoneCanJoin' ]);
allow update: if (request.auth.uid in resource.data.memberIds || resource.data.anyoneCanJoin)
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly([ 'contractIds', 'memberIds' ]);
allow delete: if request.auth.uid == resource.data.creatorId;
function isMember() { match /{somePath=**}/groupContracts/{contractId} {
return request.auth.uid in get(/databases/$(database)/documents/groups/$(groupId)).data.memberIds; allow read;
} }
match /comments/{commentId} {
match /groups/{groupId} {
allow read;
allow update: if (request.auth.uid == resource.data.creatorId || isAdmin())
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly(['name', 'about', 'anyoneCanJoin', 'aboutPostId', 'pinnedItems' ]);
allow delete: if request.auth.uid == resource.data.creatorId;
match /groupContracts/{contractId} {
allow write: if isGroupMember() || request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId
}
match /groupMembers/{memberId}{
allow create: if request.auth.uid == get(/databases/$(database)/documents/groups/$(groupId)).data.creatorId || (request.auth.uid == request.resource.data.userId && get(/databases/$(database)/documents/groups/$(groupId)).data.anyoneCanJoin);
allow delete: if request.auth.uid == resource.data.userId;
}
function isGroupMember() {
return exists(/databases/$(database)/documents/groups/$(groupId)/groupMembers/$(request.auth.uid));
}
match /comments/{commentId} {
allow read;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isGroupMember();
}
}
match /posts/{postId} {
allow read;
allow update: if isAdmin() || request.auth.uid == resource.data.creatorId
&& request.resource.data.diff(resource.data)
.affectedKeys()
.hasOnly(['name', 'content']);
allow delete: if isAdmin() || request.auth.uid == resource.data.creatorId;
match /comments/{commentId} {
allow read; allow read;
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) && isMember(); allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data) ;
} }
} }
} }
} }

3
functions/.env.dev Normal file
View File

@ -0,0 +1,3 @@
# This sets which EnvConfig is deployed to Firebase Cloud Functions
NEXT_PUBLIC_FIREBASE_ENV=DEV

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
plugins: ['lodash'], plugins: ['lodash', 'unused-imports'],
extends: ['eslint:recommended'], extends: ['eslint:recommended'],
ignorePatterns: ['dist', 'lib'], ignorePatterns: ['dist', 'lib'],
env: { env: {
@ -26,6 +26,7 @@ module.exports = {
caughtErrorsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_',
}, },
], ],
'unused-imports/no-unused-imports': 'warn',
}, },
}, },
], ],

View File

@ -17,4 +17,5 @@ package-lock.json
ui-debug.log ui-debug.log
firebase-debug.log firebase-debug.log
firestore-debug.log firestore-debug.log
pubsub-debug.log
firestore_export/ firestore_export/

View File

@ -20,7 +20,7 @@ Adapted from https://firebase.google.com/docs/functions/get-started
3. `$ firebase login` to authenticate the CLI tools to Firebase 3. `$ firebase login` to authenticate the CLI tools to Firebase
4. `$ firebase use dev` to choose the dev project 4. `$ firebase use dev` to choose the dev project
### For local development #### (Installing) For local development
0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI 0. [Install](https://cloud.google.com/sdk/docs/install) gcloud CLI
1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`): 1. If you don't have java (or see the error `Error: Process java -version has exited with code 1. Please make sure Java is installed and on your system PATH.`):
@ -35,10 +35,10 @@ Adapted from https://firebase.google.com/docs/functions/get-started
## Developing locally ## Developing locally
0. `$ firebase use dev` if you haven't already 0. `$ ./dev.sh localdb` to start the local emulator and front end
1. `$ yarn serve` to spin up the emulators 0. The Emulator UI is at http://localhost:4000; the functions are hosted on :5001. 1. If you change db trigger code, you have to start (doesn't have to complete) the deploy of it to dev to cause a hard emulator code refresh `$ firebase deploy --only functions:dbTriggerNameHere`
Note: You have to kill and restart emulators when you change code; no hot reload =( - There's surely a better way to cause/react to a db trigger update but just adding this here for now as it works
2. `$ yarn dev:emulate` in `/web` to connect to emulators with the frontend 0. Note: emulated database is cleared after every shutdown 2. If you want to test a scheduled function replace your function in `test-scheduled-function.ts` and send a GET to `http://localhost:8088/testscheduledfunction` (Best user experience is via [Postman](https://www.postman.com/downloads/)!)
## Firestore Commands ## Firestore Commands
@ -65,5 +65,6 @@ Adapted from https://firebase.google.com/docs/functions/get-started
Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). We store these using [Google Secret Manager](https://console.cloud.google.com/security/secret-manager), which provides them as environment variables to functions that require them. Some useful workflows: Secrets are strings that shouldn't be checked into Git (eg API keys, passwords). We store these using [Google Secret Manager](https://console.cloud.google.com/security/secret-manager), which provides them as environment variables to functions that require them. Some useful workflows:
- Set a secret: `$ firebase functions:secrets:set stripe.test_secret="THE-API-KEY"` - Set a secret: `$ firebase functions:secrets:set STRIPE_APIKEY`
- Then, enter the secret in the prompt.
- Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY` - Read a secret: `$ firebase functions:secrets:access STRIPE_APIKEY`

View File

@ -5,17 +5,19 @@
"firestore": "dev-mantic-markets.appspot.com" "firestore": "dev-mantic-markets.appspot.com"
}, },
"scripts": { "scripts": {
"build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env dist", "build": "yarn compile && rm -rf dist && mkdir -p dist/functions && cp -R ../common/lib dist/common && cp -R lib/src dist/functions/src && cp ../yarn.lock dist && cp package.json dist && cp .env.prod dist && cp .env.dev dist",
"compile": "tsc -b", "compile": "tsc -b",
"watch": "tsc -w", "watch": "tsc -w",
"shell": "yarn build && firebase functions:shell", "shell": "yarn build && firebase functions:shell",
"start": "yarn shell", "start": "yarn shell",
"deploy": "firebase deploy --only functions", "deploy": "firebase deploy --only functions",
"logs": "firebase functions:log", "logs": "firebase functions:log",
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore --import=./firestore_export", "dev": "nodemon src/serve.ts",
"db:update-local-from-remote": "yarn db:backup-remote && gsutil rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export", "localDbScript": "firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"serve": "firebase use dev && yarn build && firebase emulators:start --only functions,firestore,pubsub --import=./firestore_export",
"db:update-local-from-remote": "yarn db:backup-remote && gsutil -m rsync -r gs://$npm_package_config_firestore/firestore_export ./firestore_export",
"db:backup-local": "firebase emulators:export --force ./firestore_export", "db:backup-local": "firebase emulators:export --force ./firestore_export",
"db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)", "db:rename-remote-backup-folder": "gsutil -m mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)",
"db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/", "db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/",
"verify": "(cd .. && yarn verify)", "verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty" "verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty"
@ -24,22 +26,33 @@
"dependencies": { "dependencies": {
"@amplitude/node": "1.10.0", "@amplitude/node": "1.10.0",
"@google-cloud/functions-framework": "3.1.2", "@google-cloud/functions-framework": "3.1.2",
"@tiptap/core": "2.0.0-beta.181", "@tiptap/core": "2.0.0-beta.199",
"@tiptap/extension-image": "2.0.0-beta.30", "@tiptap/extension-image": "2.0.0-beta.199",
"@tiptap/extension-link": "2.0.0-beta.43", "@tiptap/extension-link": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.190", "@tiptap/extension-mention": "2.0.0-beta.199",
"@tiptap/html": "2.0.0-beta.199",
"@tiptap/starter-kit": "2.0.0-beta.199",
"@tiptap/suggestion": "2.0.0-beta.199",
"cors": "2.8.5",
"dayjs": "1.11.4",
"express": "4.18.1",
"firebase-admin": "10.0.0", "firebase-admin": "10.0.0",
"firebase-functions": "3.21.2", "firebase-functions": "3.21.2",
"lodash": "4.17.21", "lodash": "4.17.21",
"mailgun-js": "0.22.0", "mailgun-js": "0.22.0",
"marked": "4.1.1",
"module-alias": "2.2.2", "module-alias": "2.2.2",
"node-fetch": "2",
"stripe": "8.194.0", "stripe": "8.194.0",
"zod": "3.17.2" "zod": "3.17.2"
}, },
"devDependencies": { "devDependencies": {
"@types/mailgun-js": "0.22.12", "@types/mailgun-js": "0.22.12",
"@types/marked": "4.0.7",
"@types/module-alias": "2.0.1", "@types/module-alias": "2.0.1",
"firebase-functions-test": "0.3.3" "@types/node-fetch": "2.6.2",
"firebase-functions-test": "0.3.3",
"puppeteer": "18.0.5"
}, },
"private": true "private": true
} }

View File

@ -0,0 +1,170 @@
import { z } from 'zod'
import { APIError, newEndpoint, validate } from './api'
import { log } from './utils'
import { Contract, CPMMBinaryContract } from '../../common/contract'
import { User } from '../../common/user'
import * as admin from 'firebase-admin'
import { FieldValue } from 'firebase-admin/firestore'
import { removeUndefinedProps } from '../../common/util/object'
import { Acceptance, Challenge } from '../../common/challenge'
import { CandidateBet } from '../../common/new-bet'
import { createChallengeAcceptedNotification } from './create-notification'
import { noFees } from '../../common/fees'
import { formatMoney, formatPercent } from '../../common/util/format'
import { redeemShares } from './redeem-shares'
const bodySchema = z.object({
contractId: z.string(),
challengeSlug: z.string(),
outcomeType: z.literal('BINARY'),
closeTime: z.number().gte(Date.now()),
})
const firestore = admin.firestore()
export const acceptchallenge = newEndpoint({}, async (req, auth) => {
const { challengeSlug, contractId } = validate(bodySchema, req.body)
const result = await firestore.runTransaction(async (trans) => {
const contractDoc = firestore.doc(`contracts/${contractId}`)
const userDoc = firestore.doc(`users/${auth.uid}`)
const challengeDoc = firestore.doc(
`contracts/${contractId}/challenges/${challengeSlug}`
)
const [contractSnap, userSnap, challengeSnap] = await trans.getAll(
contractDoc,
userDoc,
challengeDoc
)
if (!contractSnap.exists) throw new APIError(400, 'Contract not found.')
if (!userSnap.exists) throw new APIError(400, 'User not found.')
if (!challengeSnap.exists) throw new APIError(400, 'Challenge not found.')
const anyContract = contractSnap.data() as Contract
const user = userSnap.data() as User
const challenge = challengeSnap.data() as Challenge
if (challenge.acceptances.length > 0)
throw new APIError(400, 'Challenge already accepted.')
const creatorDoc = firestore.doc(`users/${challenge.creatorId}`)
const creatorSnap = await trans.get(creatorDoc)
if (!creatorSnap.exists) throw new APIError(400, 'Creator not found.')
const creator = creatorSnap.data() as User
const {
creatorAmount,
acceptorOutcome,
creatorOutcome,
creatorOutcomeProb,
acceptorAmount,
} = challenge
if (user.balance < acceptorAmount)
throw new APIError(400, 'Insufficient balance.')
if (creator.balance < creatorAmount)
throw new APIError(400, 'Creator has insufficient balance.')
const contract = anyContract as CPMMBinaryContract
const shares = (1 / creatorOutcomeProb) * creatorAmount
const createdTime = Date.now()
const probOfYes =
creatorOutcome === 'YES' ? creatorOutcomeProb : 1 - creatorOutcomeProb
log(
'Creating challenge bet for',
user.username,
shares,
acceptorOutcome,
'shares',
'at',
formatPercent(creatorOutcomeProb),
'for',
formatMoney(acceptorAmount)
)
const yourNewBet: CandidateBet = removeUndefinedProps({
orderAmount: acceptorAmount,
amount: acceptorAmount,
shares,
isCancelled: false,
contractId: contract.id,
outcome: acceptorOutcome,
probBefore: probOfYes,
probAfter: probOfYes,
loanAmount: 0,
createdTime,
fees: noFees,
challengeSlug: challenge.slug,
})
const yourNewBetDoc = contractDoc.collection('bets').doc()
trans.create(yourNewBetDoc, {
id: yourNewBetDoc.id,
userId: user.id,
...yourNewBet,
})
trans.update(userDoc, { balance: FieldValue.increment(-yourNewBet.amount) })
const creatorNewBet: CandidateBet = removeUndefinedProps({
orderAmount: creatorAmount,
amount: creatorAmount,
shares,
isCancelled: false,
contractId: contract.id,
outcome: creatorOutcome,
probBefore: probOfYes,
probAfter: probOfYes,
loanAmount: 0,
createdTime,
fees: noFees,
challengeSlug: challenge.slug,
})
const creatorBetDoc = contractDoc.collection('bets').doc()
trans.create(creatorBetDoc, {
id: creatorBetDoc.id,
userId: creator.id,
...creatorNewBet,
})
trans.update(creatorDoc, {
balance: FieldValue.increment(-creatorNewBet.amount),
})
const volume = contract.volume + yourNewBet.amount + creatorNewBet.amount
trans.update(contractDoc, { volume })
trans.update(
challengeDoc,
removeUndefinedProps({
acceptedByUserIds: [user.id],
acceptances: [
{
userId: user.id,
betId: yourNewBetDoc.id,
createdTime,
amount: acceptorAmount,
userUsername: user.username,
userName: user.name,
userAvatarUrl: user.avatarUrl,
} as Acceptance,
],
})
)
await createChallengeAcceptedNotification(
user,
creator,
challenge,
acceptorAmount,
contract
)
log('Done, sent notification.')
return yourNewBetDoc
})
await redeemShares(auth.uid, contractId)
return { betId: result.id }
})

View File

@ -1,9 +1,8 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import { Contract } from '../../common/contract' import { Contract, CPMMContract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { removeUndefinedProps } from '../../common/util/object'
import { getNewLiquidityProvision } from '../../common/add-liquidity' import { getNewLiquidityProvision } from '../../common/add-liquidity'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
@ -12,10 +11,10 @@ const bodySchema = z.object({
amount: z.number().gt(0), amount: z.number().gt(0),
}) })
export const addliquidity = newEndpoint({}, async (req, auth) => { export const addsubsidy = newEndpoint({}, async (req, auth) => {
const { amount, contractId } = validate(bodySchema, req.body) const { amount, contractId } = validate(bodySchema, req.body)
if (!isFinite(amount)) throw new APIError(400, 'Invalid amount') if (!isFinite(amount) || amount < 1) throw new APIError(400, 'Invalid amount')
// run as transaction to prevent race conditions // run as transaction to prevent race conditions
return await firestore.runTransaction(async (transaction) => { return await firestore.runTransaction(async (transaction) => {
@ -45,29 +44,18 @@ export const addliquidity = newEndpoint({}, async (req, auth) => {
.collection(`contracts/${contractId}/liquidity`) .collection(`contracts/${contractId}/liquidity`)
.doc() .doc()
const { newLiquidityProvision, newPool, newP, newTotalLiquidity } = const { newLiquidityProvision, newTotalLiquidity, newSubsidyPool } =
getNewLiquidityProvision( getNewLiquidityProvision(
user, user.id,
amount, amount,
contract, contract,
newLiquidityProvisionDoc.id newLiquidityProvisionDoc.id
) )
if (newP !== undefined && !isFinite(newP)) { transaction.update(contractDoc, {
return { subsidyPool: newSubsidyPool,
status: 'error', totalLiquidity: newTotalLiquidity,
message: 'Liquidity injection rejected due to overflow error.', } as Partial<CPMMContract>)
}
}
transaction.update(
contractDoc,
removeUndefinedProps({
pool: newPool,
p: newP,
totalLiquidity: newTotalLiquidity,
})
)
const newBalance = user.balance - amount const newBalance = user.balance - amount
const newTotalDeposits = user.totalDeposits - amount const newTotalDeposits = user.totalDeposits - amount

View File

@ -3,7 +3,7 @@ import * as Amplitude from '@amplitude/node'
import { DEV_CONFIG } from '../../common/envs/dev' import { DEV_CONFIG } from '../../common/envs/dev'
import { PROD_CONFIG } from '../../common/envs/prod' import { PROD_CONFIG } from '../../common/envs/prod'
import { isProd } from './utils' import { isProd, tryOrLogError } from './utils'
const key = isProd() ? PROD_CONFIG.amplitudeApiKey : DEV_CONFIG.amplitudeApiKey const key = isProd() ? PROD_CONFIG.amplitudeApiKey : DEV_CONFIG.amplitudeApiKey
@ -15,10 +15,12 @@ export const track = async (
eventProperties?: any, eventProperties?: any,
amplitudeProperties?: Partial<Amplitude.Event> amplitudeProperties?: Partial<Amplitude.Event>
) => { ) => {
await amp.logEvent({ return await tryOrLogError(
event_type: eventName, amp.logEvent({
user_id: userId, event_type: eventName,
event_properties: eventProperties, user_id: userId,
...amplitudeProperties, event_properties: eventProperties,
}) ...amplitudeProperties,
})
)
} }

View File

@ -1,6 +1,7 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { logger } from 'firebase-functions/v2' import { Request, RequestHandler, Response } from 'express'
import { HttpsOptions, onRequest, Request } from 'firebase-functions/v2/https' import { error } from 'firebase-functions/logger'
import { HttpsOptions } from 'firebase-functions/v2/https'
import { log } from './utils' import { log } from './utils'
import { z } from 'zod' import { z } from 'zod'
import { APIError } from '../../common/api' import { APIError } from '../../common/api'
@ -13,7 +14,7 @@ import {
export { APIError } from '../../common/api' export { APIError } from '../../common/api'
type Output = Record<string, unknown> type Output = Record<string, unknown>
type AuthedUser = { export type AuthedUser = {
uid: string uid: string
creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser }) creds: JwtCredentials | (KeyCredentials & { privateUser: PrivateUser })
} }
@ -22,13 +23,8 @@ type JwtCredentials = { kind: 'jwt'; data: admin.auth.DecodedIdToken }
type KeyCredentials = { kind: 'key'; data: string } type KeyCredentials = { kind: 'key'; data: string }
type Credentials = JwtCredentials | KeyCredentials type Credentials = JwtCredentials | KeyCredentials
const auth = admin.auth()
const firestore = admin.firestore()
const privateUsers = firestore.collection(
'private-users'
) as admin.firestore.CollectionReference<PrivateUser>
export const parseCredentials = async (req: Request): Promise<Credentials> => { export const parseCredentials = async (req: Request): Promise<Credentials> => {
const auth = admin.auth()
const authHeader = req.get('Authorization') const authHeader = req.get('Authorization')
if (!authHeader) { if (!authHeader) {
throw new APIError(403, 'Missing Authorization header.') throw new APIError(403, 'Missing Authorization header.')
@ -45,7 +41,7 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
return { kind: 'jwt', data: await auth.verifyIdToken(payload) } return { kind: 'jwt', data: await auth.verifyIdToken(payload) }
} catch (err) { } catch (err) {
// This is somewhat suspicious, so get it into the firebase console // This is somewhat suspicious, so get it into the firebase console
logger.error('Error verifying Firebase JWT: ', err) error('Error verifying Firebase JWT: ', err)
throw new APIError(403, 'Error validating token.') throw new APIError(403, 'Error validating token.')
} }
case 'Key': case 'Key':
@ -56,6 +52,8 @@ export const parseCredentials = async (req: Request): Promise<Credentials> => {
} }
export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => { export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
const firestore = admin.firestore()
const privateUsers = firestore.collection('private-users')
switch (creds.kind) { switch (creds.kind) {
case 'jwt': { case 'jwt': {
if (typeof creds.data.user_id !== 'string') { if (typeof creds.data.user_id !== 'string') {
@ -69,7 +67,7 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
if (privateUserQ.empty) { if (privateUserQ.empty) {
throw new APIError(403, `No private user exists with API key ${key}.`) throw new APIError(403, `No private user exists with API key ${key}.`)
} }
const privateUser = privateUserQ.docs[0].data() const privateUser = privateUserQ.docs[0].data() as PrivateUser
return { uid: privateUser.id, creds: { privateUser, ...creds } } return { uid: privateUser.id, creds: { privateUser, ...creds } }
} }
default: default:
@ -77,12 +75,30 @@ export const lookupUser = async (creds: Credentials): Promise<AuthedUser> => {
} }
} }
export const writeResponseError = (e: unknown, res: Response) => {
if (e instanceof APIError) {
const output: { [k: string]: unknown } = { message: e.message }
if (e.details != null) {
output.details = e.details
}
res.status(e.code).json(output)
} else {
error(e)
res.status(500).json({ message: 'An unknown error occurred.' })
}
}
export const zTimestamp = () => { export const zTimestamp = () => {
return z.preprocess((arg) => { return z.preprocess((arg) => {
return typeof arg == 'number' ? new Date(arg) : undefined return typeof arg == 'number' ? new Date(arg) : undefined
}, z.date()) }, z.date())
} }
export type EndpointDefinition = {
opts: EndpointOptions & { method: string }
handler: RequestHandler
}
export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => { export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
const result = schema.safeParse(val) const result = schema.safeParse(val)
if (!result.success) { if (!result.success) {
@ -99,12 +115,12 @@ export const validate = <T extends z.ZodTypeAny>(schema: T, val: unknown) => {
} }
} }
interface EndpointOptions extends HttpsOptions { export interface EndpointOptions extends HttpsOptions {
methods?: string[] method?: string
} }
const DEFAULT_OPTS = { const DEFAULT_OPTS = {
methods: ['POST'], method: 'POST',
minInstances: 1, minInstances: 1,
concurrency: 100, concurrency: 100,
memory: '2GiB', memory: '2GiB',
@ -113,28 +129,41 @@ const DEFAULT_OPTS = {
} }
export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => { export const newEndpoint = (endpointOpts: EndpointOptions, fn: Handler) => {
const opts = Object.assign(endpointOpts, DEFAULT_OPTS) const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
return onRequest(opts, async (req, res) => { return {
log('Request processing started.') opts,
try { handler: async (req: Request, res: Response) => {
if (!opts.methods.includes(req.method)) { log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
const allowed = opts.methods.join(', ') try {
throw new APIError(405, `This endpoint supports only ${allowed}.`) if (opts.method !== req.method) {
} throw new APIError(405, `This endpoint supports only ${opts.method}.`)
const authedUser = await lookupUser(await parseCredentials(req))
log('User credentials processed.')
res.status(200).json(await fn(req, authedUser))
} catch (e) {
if (e instanceof APIError) {
const output: { [k: string]: unknown } = { message: e.message }
if (e.details != null) {
output.details = e.details
} }
res.status(e.code).json(output) const authedUser = await lookupUser(await parseCredentials(req))
} else { res.status(200).json(await fn(req, authedUser))
logger.error(e) } catch (e) {
res.status(500).json({ message: 'An unknown error occurred.' }) writeResponseError(e, res)
} }
} },
}) } as EndpointDefinition
}
export const newEndpointNoAuth = (
endpointOpts: EndpointOptions,
fn: (req: Request) => Promise<Output>
) => {
const opts = Object.assign({}, DEFAULT_OPTS, endpointOpts)
return {
opts,
handler: async (req: Request, res: Response) => {
log(`${req.method} ${req.url} ${JSON.stringify(req.body)}`)
try {
if (opts.method !== req.method) {
throw new APIError(405, `This endpoint supports only ${opts.method}.`)
}
res.status(200).json(await fn(req))
} catch (e) {
writeResponseError(e, res)
}
},
} as EndpointDefinition
} }

View File

@ -10,7 +10,7 @@ const bodySchema = z.object({
export const cancelbet = newEndpoint({}, async (req, auth) => { export const cancelbet = newEndpoint({}, async (req, auth) => {
const { betId } = validate(bodySchema, req.body) const { betId } = validate(bodySchema, req.body)
const result = await firestore.runTransaction(async (trans) => { return await firestore.runTransaction(async (trans) => {
const snap = await trans.get( const snap = await trans.get(
firestore.collectionGroup('bets').where('id', '==', betId) firestore.collectionGroup('bets').where('id', '==', betId)
) )
@ -28,8 +28,6 @@ export const cancelbet = newEndpoint({}, async (req, auth) => {
return { ...bet, isCancelled: true } return { ...bet, isCancelled: true }
}) })
return result
}) })
const firestore = admin.firestore() const firestore = admin.firestore()

View File

@ -2,6 +2,7 @@ import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import { getUser } from './utils' import { getUser } from './utils'
import { Bet } from '../../common/bet'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { Comment } from '../../common/comment' import { Comment } from '../../common/comment'
import { User } from '../../common/user' import { User } from '../../common/user'
@ -37,6 +38,56 @@ export const changeUser = async (
avatarUrl?: string avatarUrl?: string
} }
) => { ) => {
// Update contracts, comments, and answers outside of a transaction to avoid contention.
// Using bulkWriter to supports >500 writes at a time
const contractsRef = firestore
.collection('contracts')
.where('creatorId', '==', user.id)
const contracts = await contractsRef.get()
const contractUpdate: Partial<Contract> = removeUndefinedProps({
creatorName: update.name,
creatorUsername: update.username,
creatorAvatarUrl: update.avatarUrl,
})
const commentSnap = await firestore
.collectionGroup('comments')
.where('userUsername', '==', user.username)
.get()
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const answerSnap = await firestore
.collectionGroup('answers')
.where('username', '==', user.username)
.get()
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
const betsSnap = await firestore
.collectionGroup('bets')
.where('userId', '==', user.id)
.get()
const betsUpdate: Partial<Bet> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const bulkWriter = firestore.bulkWriter()
commentSnap.docs.forEach((d) => bulkWriter.update(d.ref, commentUpdate))
answerSnap.docs.forEach((d) => bulkWriter.update(d.ref, answerUpdate))
contracts.docs.forEach((d) => bulkWriter.update(d.ref, contractUpdate))
betsSnap.docs.forEach((d) => bulkWriter.update(d.ref, betsUpdate))
await bulkWriter.flush()
console.log('Done writing!')
// Update the username inside a transaction
return await firestore.runTransaction(async (transaction) => { return await firestore.runTransaction(async (transaction) => {
if (update.username) { if (update.username) {
update.username = cleanUsername(update.username) update.username = cleanUsername(update.username)
@ -58,42 +109,7 @@ export const changeUser = async (
const userRef = firestore.collection('users').doc(user.id) const userRef = firestore.collection('users').doc(user.id)
const userUpdate: Partial<User> = removeUndefinedProps(update) const userUpdate: Partial<User> = removeUndefinedProps(update)
const contractsRef = firestore
.collection('contracts')
.where('creatorId', '==', user.id)
const contracts = await transaction.get(contractsRef)
const contractUpdate: Partial<Contract> = removeUndefinedProps({
creatorName: update.name,
creatorUsername: update.username,
creatorAvatarUrl: update.avatarUrl,
})
const commentSnap = await transaction.get(
firestore
.collectionGroup('comments')
.where('userUsername', '==', user.username)
)
const commentUpdate: Partial<Comment> = removeUndefinedProps({
userName: update.name,
userUsername: update.username,
userAvatarUrl: update.avatarUrl,
})
const answerSnap = await transaction.get(
firestore
.collectionGroup('answers')
.where('username', '==', user.username)
)
const answerUpdate: Partial<Answer> = removeUndefinedProps(update)
transaction.update(userRef, userUpdate) transaction.update(userRef, userUpdate)
commentSnap.docs.forEach((d) => transaction.update(d.ref, commentUpdate))
answerSnap.docs.forEach((d) => transaction.update(d.ref, answerUpdate))
contracts.docs.forEach((d) => transaction.update(d.ref, contractUpdate))
}) })
} }

View File

@ -0,0 +1,58 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import { Contract } from '../../common/contract'
import { getUser } from './utils'
import { isAdmin, isManifoldId } from '../../common/envs/constants'
import { APIError, newEndpoint, validate } from './api'
const bodySchema = z.object({
contractId: z.string(),
closeTime: z.number().int().nonnegative().optional(),
})
export const closemarket = newEndpoint({}, async (req, auth) => {
const { contractId, closeTime } = validate(bodySchema, req.body)
const contractDoc = firestore.doc(`contracts/${contractId}`)
const contractSnap = await contractDoc.get()
if (!contractSnap.exists)
throw new APIError(404, 'No contract exists with the provided ID')
const contract = contractSnap.data() as Contract
const { creatorId } = contract
const firebaseUser = await admin.auth().getUser(auth.uid)
if (
creatorId !== auth.uid &&
!isManifoldId(auth.uid) &&
!isAdmin(firebaseUser.email)
)
throw new APIError(403, 'User is not creator of contract')
const now = Date.now()
if (!closeTime && contract.closeTime && contract.closeTime < now)
throw new APIError(400, 'Contract already closed')
if (closeTime && closeTime < now)
throw new APIError(
400,
'Close time must be in the future. ' +
'Alternatively, do not provide a close time to close immediately.'
)
const creator = await getUser(creatorId)
if (!creator) throw new APIError(500, 'Creator not found')
const updatedContract = {
...contract,
closeTime: closeTime ? closeTime : now,
}
await contractDoc.update(updatedContract)
console.log('contract ', contractId, 'closed')
return updatedContract
})
const firestore = admin.firestore()

View File

@ -5,9 +5,9 @@ import { Contract } from '../../common/contract'
import { User } from '../../common/user' import { User } from '../../common/user'
import { getNewMultiBetInfo } from '../../common/new-bet' import { getNewMultiBetInfo } from '../../common/new-bet'
import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer' import { Answer, MAX_ANSWER_LENGTH } from '../../common/answer'
import { getContract, getValues } from './utils' import { getValues } from './utils'
import { sendNewAnswerEmail } from './emails'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { addUserToContractFollowers } from './follow-market'
const bodySchema = z.object({ const bodySchema = z.object({
contractId: z.string().max(MAX_ANSWER_LENGTH), contractId: z.string().max(MAX_ANSWER_LENGTH),
@ -75,10 +75,8 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
} }
transaction.create(newAnswerDoc, answer) transaction.create(newAnswerDoc, answer)
const loanAmount = 0
const { newBet, newPool, newTotalShares, newTotalBets } = const { newBet, newPool, newTotalShares, newTotalBets } =
getNewMultiBetInfo(answerId, amount, contract, loanAmount) getNewMultiBetInfo(answerId, amount, contract)
const newBalance = user.balance - amount const newBalance = user.balance - amount
const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc() const betDoc = firestore.collection(`contracts/${contractId}/bets`).doc()
@ -99,9 +97,7 @@ export const createanswer = newEndpoint(opts, async (req, auth) => {
return answer return answer
}) })
const contract = await getContract(contractId) await addUserToContractFollowers(contractId, auth.uid)
if (answer && contract) await sendNewAnswerEmail(answer, contract)
return answer return answer
}) })

View File

@ -0,0 +1,105 @@
import * as admin from 'firebase-admin'
import { getContract, getUser, log } from './utils'
import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core'
import { z } from 'zod'
import { removeUndefinedProps } from '../../common/util/object'
import { htmlToRichText } from '../../common/util/parse'
import { marked } from 'marked'
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(contentSchema).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const postSchema = z.object({
contractId: z.string(),
content: contentSchema.optional(),
html: z.string().optional(),
markdown: z.string().optional(),
})
const MAX_COMMENT_JSON_LENGTH = 20000
// For now, only supports creating a new top-level comment on a contract.
// Replies, posts, chats are not supported yet.
export const createcomment = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { contractId, content, html, markdown } = validate(postSchema, req.body)
const creator = await getUser(auth.uid)
const contract = await getContract(contractId)
if (!creator) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
if (!contract) {
throw new APIError(400, 'No contract exists with the given ID.')
}
let contentJson = null
if (content) {
contentJson = content
} else if (html) {
console.log('html', html)
contentJson = htmlToRichText(html)
} else if (markdown) {
const markedParse = marked.parse(markdown)
log('parsed', markedParse)
contentJson = htmlToRichText(markedParse)
log('json', contentJson)
}
if (!contentJson) {
throw new APIError(400, 'No comment content provided.')
}
if (JSON.stringify(contentJson).length > MAX_COMMENT_JSON_LENGTH) {
throw new APIError(
400,
`Comment is too long; should be less than ${MAX_COMMENT_JSON_LENGTH} as a JSON string.`
)
}
const ref = firestore.collection(`contracts/${contractId}/comments`).doc()
const comment = removeUndefinedProps({
id: ref.id,
content: contentJson,
createdTime: Date.now(),
userId: creator.id,
userName: creator.name,
userUsername: creator.username,
userAvatarUrl: creator.avatarUrl,
// OnContract fields
commentType: 'contract',
contractId: contractId,
contractSlug: contract.slug,
contractQuestion: contract.question,
})
await ref.set(comment)
return { status: 'success', comment }
})

View File

@ -1,242 +0,0 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import {
CPMMBinaryContract,
Contract,
FreeResponseContract,
MAX_QUESTION_LENGTH,
MAX_TAG_LENGTH,
NumericContract,
OUTCOME_TYPES,
} from '../../common/contract'
import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random'
import { chargeUser } from './utils'
import { APIError, newEndpoint, validate, zTimestamp } from './api'
import {
FIXED_ANTE,
getCpmmInitialLiquidity,
getFreeAnswerAnte,
getNumericAnte,
} from '../../common/antes'
import { getNoneAnswer } from '../../common/answer'
import { getNewContract } from '../../common/new-contract'
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
import { User } from '../../common/user'
import { Group, MAX_ID_LENGTH } from '../../common/group'
import { getPseudoProbability } from '../../common/pseudo-numeric'
import { JSONContent } from '@tiptap/core'
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(descScehma).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const bodySchema = z.object({
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
description: descScehma.optional(),
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
closeTime: zTimestamp().refine(
(date) => date.getTime() > new Date().getTime(),
'Close time must be in the future.'
),
outcomeType: z.enum(OUTCOME_TYPES),
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
})
const binarySchema = z.object({
initialProb: z.number().min(1).max(99),
})
const finite = () =>
z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER)
const numericSchema = z.object({
min: finite(),
max: finite(),
initialValue: finite(),
isLogScale: z.boolean().optional(),
})
export const createmarket = newEndpoint({}, async (req, auth) => {
const { question, description, tags, closeTime, outcomeType, groupId } =
validate(bodySchema, req.body)
let min, max, initialProb, isLogScale
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
let initialValue
;({ min, max, initialValue, isLogScale } = validate(
numericSchema,
req.body
))
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
throw new APIError(400, 'Invalid range.')
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
if (initialProb < 1 || initialProb > 99)
throw new APIError(400, 'Invalid initial value.')
}
if (outcomeType === 'BINARY') {
;({ initialProb } = validate(binarySchema, req.body))
}
const userDoc = await firestore.collection('users').doc(auth.uid).get()
if (!userDoc.exists) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
const user = userDoc.data() as User
const ante = FIXED_ANTE
// TODO: this is broken because it's not in a transaction
if (ante > user.balance)
throw new APIError(400, `Balance must be at least ${ante}.`)
const slug = await getSlug(question)
const contractRef = firestore.collection('contracts').doc()
let group = null
if (groupId) {
const groupDocRef = await firestore.collection('groups').doc(groupId)
const groupDoc = await groupDocRef.get()
if (!groupDoc.exists) {
throw new APIError(400, 'No group exists with the given group ID.')
}
group = groupDoc.data() as Group
if (!group.memberIds.includes(user.id)) {
throw new APIError(
400,
'User must be a member of the group to add markets to it.'
)
}
if (!group.contractIds.includes(contractRef.id))
await groupDocRef.update({
contractIds: [...group.contractIds, contractRef.id],
})
}
console.log(
'creating contract for',
user.username,
'on',
question,
'ante:',
ante || 0
)
const contract = getNewContract(
contractRef.id,
slug,
user,
question,
outcomeType,
description ?? {},
initialProb ?? 0,
ante,
closeTime.getTime(),
tags ?? [],
NUMERIC_BUCKET_COUNT,
min ?? 0,
max ?? 0,
isLogScale ?? false
)
if (ante) await chargeUser(user.id, ante, true)
await contractRef.create(contract)
const providerId = user.id
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
const liquidityDoc = firestore
.collection(`contracts/${contract.id}/liquidity`)
.doc()
const lp = getCpmmInitialLiquidity(
providerId,
contract as CPMMBinaryContract,
liquidityDoc.id,
ante
)
await liquidityDoc.set(lp)
} else if (outcomeType === 'FREE_RESPONSE') {
const noneAnswerDoc = firestore
.collection(`contracts/${contract.id}/answers`)
.doc('0')
const noneAnswer = getNoneAnswer(contract.id, user)
await noneAnswerDoc.set(noneAnswer)
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getFreeAnswerAnte(
providerId,
contract as FreeResponseContract,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
} else if (outcomeType === 'NUMERIC') {
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getNumericAnte(
providerId,
contract as NumericContract,
ante,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
}
return contract
})
const getSlug = async (question: string) => {
const proposedSlug = slugify(question)
const preexistingContract = await getContractFromSlug(proposedSlug)
return preexistingContract
? proposedSlug + '-' + randomString()
: proposedSlug
}
const firestore = admin.firestore()
export async function getContractFromSlug(slug: string) {
const snap = await firestore
.collection('contracts')
.where('slug', '==', slug)
.get()
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
}

View File

@ -10,7 +10,7 @@ import {
MAX_GROUP_NAME_LENGTH, MAX_GROUP_NAME_LENGTH,
MAX_ID_LENGTH, MAX_ID_LENGTH,
} from '../../common/group' } from '../../common/group'
import { APIError, newEndpoint, validate } from '../../functions/src/api' import { APIError, newEndpoint, validate } from './api'
import { z } from 'zod' import { z } from 'zod'
const bodySchema = z.object({ const bodySchema = z.object({
@ -21,6 +21,7 @@ const bodySchema = z.object({
}) })
export const creategroup = newEndpoint({}, async (req, auth) => { export const creategroup = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { name, about, memberIds, anyoneCanJoin } = validate( const { name, about, memberIds, anyoneCanJoin } = validate(
bodySchema, bodySchema,
req.body req.body
@ -57,17 +58,29 @@ export const creategroup = newEndpoint({}, async (req, auth) => {
createdTime: Date.now(), createdTime: Date.now(),
mostRecentActivityTime: Date.now(), mostRecentActivityTime: Date.now(),
// TODO: allow users to add contract ids on group creation // TODO: allow users to add contract ids on group creation
contractIds: [],
anyoneCanJoin, anyoneCanJoin,
memberIds, totalContracts: 0,
totalMembers: memberIds.length,
postIds: [],
pinnedItems: [],
} }
await groupRef.create(group) await groupRef.create(group)
// create a GroupMemberDoc for each member
await Promise.all(
memberIds.map((memberId) =>
groupRef.collection('groupMembers').doc(memberId).create({
userId: memberId,
createdTime: Date.now(),
})
)
)
return { status: 'success', group: group } return { status: 'success', group: group }
}) })
const getSlug = async (name: string) => { export const getSlug = async (name: string) => {
const proposedSlug = slugify(name) const proposedSlug = slugify(name)
const preexistingGroup = await getGroupFromSlug(proposedSlug) const preexistingGroup = await getGroupFromSlug(proposedSlug)
@ -75,9 +88,8 @@ const getSlug = async (name: string) => {
return preexistingGroup ? proposedSlug + '-' + randomString() : proposedSlug return preexistingGroup ? proposedSlug + '-' + randomString() : proposedSlug
} }
const firestore = admin.firestore()
export async function getGroupFromSlug(slug: string) { export async function getGroupFromSlug(slug: string) {
const firestore = admin.firestore()
const snap = await firestore const snap = await firestore
.collection('groups') .collection('groups')
.where('slug', '==', slug) .where('slug', '==', slug)

View File

@ -0,0 +1,383 @@
import * as admin from 'firebase-admin'
import { z } from 'zod'
import {
Contract,
CPMMBinaryContract,
FreeResponseContract,
MAX_QUESTION_LENGTH,
MAX_TAG_LENGTH,
MultipleChoiceContract,
NumericContract,
OUTCOME_TYPES,
VISIBILITIES,
} from '../../common/contract'
import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random'
import { chargeUser, getContract, isProd } from './utils'
import { APIError, AuthedUser, newEndpoint, validate, zTimestamp } from './api'
import { FIXED_ANTE, FREE_MARKETS_PER_USER_MAX } from '../../common/economy'
import {
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
getCpmmInitialLiquidity,
getFreeAnswerAnte,
getMultipleChoiceAntes,
getNumericAnte,
HOUSE_LIQUIDITY_PROVIDER_ID,
} from '../../common/antes'
import { Answer, getNoneAnswer } from '../../common/answer'
import { getNewContract } from '../../common/new-contract'
import { NUMERIC_BUCKET_COUNT } from '../../common/numeric-constants'
import { User } from '../../common/user'
import { Group, GroupLink, MAX_ID_LENGTH } from '../../common/group'
import { getPseudoProbability } from '../../common/pseudo-numeric'
import { JSONContent } from '@tiptap/core'
import { uniq, zip } from 'lodash'
import { Bet } from '../../common/bet'
import { FieldValue } from 'firebase-admin/firestore'
const descScehma: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(descScehma).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const bodySchema = z.object({
question: z.string().min(1).max(MAX_QUESTION_LENGTH),
description: descScehma.or(z.string()).optional(),
tags: z.array(z.string().min(1).max(MAX_TAG_LENGTH)).optional(),
closeTime: zTimestamp().refine(
(date) => date.getTime() > new Date().getTime(),
'Close time must be in the future.'
),
outcomeType: z.enum(OUTCOME_TYPES),
groupId: z.string().min(1).max(MAX_ID_LENGTH).optional(),
visibility: z.enum(VISIBILITIES).optional(),
})
const binarySchema = z.object({
initialProb: z.number().min(1).max(99),
})
const finite = () =>
z.number().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER)
const numericSchema = z.object({
min: finite(),
max: finite(),
initialValue: finite(),
isLogScale: z.boolean().optional(),
})
const multipleChoiceSchema = z.object({
answers: z.string().trim().min(1).array().min(2),
})
export const createmarket = newEndpoint({}, (req, auth) => {
return createMarketHelper(req.body, auth)
})
export async function createMarketHelper(body: any, auth: AuthedUser) {
const {
question,
description,
tags,
closeTime,
outcomeType,
groupId,
visibility = 'public',
} = validate(bodySchema, body)
let min, max, initialProb, isLogScale, answers
if (outcomeType === 'PSEUDO_NUMERIC' || outcomeType === 'NUMERIC') {
let initialValue
;({ min, max, initialValue, isLogScale } = validate(numericSchema, body))
if (max - min <= 0.01 || initialValue <= min || initialValue >= max)
throw new APIError(400, 'Invalid range.')
initialProb = getPseudoProbability(initialValue, min, max, isLogScale) * 100
if (initialProb < 1 || initialProb > 99)
if (outcomeType === 'PSEUDO_NUMERIC')
throw new APIError(
400,
`Initial value is too ${initialProb < 1 ? 'low' : 'high'}`
)
else throw new APIError(400, 'Invalid initial probability.')
}
if (outcomeType === 'BINARY') {
;({ initialProb } = validate(binarySchema, body))
}
if (outcomeType === 'MULTIPLE_CHOICE') {
;({ answers } = validate(multipleChoiceSchema, body))
}
const userDoc = await firestore.collection('users').doc(auth.uid).get()
if (!userDoc.exists) {
throw new APIError(400, 'No user exists with the authenticated user ID.')
}
const user = userDoc.data() as User
const ante = FIXED_ANTE
const deservesFreeMarket =
(user?.freeMarketsCreated ?? 0) < FREE_MARKETS_PER_USER_MAX
// TODO: this is broken because it's not in a transaction
if (ante > user.balance && !deservesFreeMarket)
throw new APIError(400, `Balance must be at least ${ante}.`)
let group: Group | null = null
if (groupId) {
const groupDocRef = firestore.collection('groups').doc(groupId)
const groupDoc = await groupDocRef.get()
if (!groupDoc.exists) {
throw new APIError(400, 'No group exists with the given group ID.')
}
group = groupDoc.data() as Group
const groupMembersSnap = await firestore
.collection(`groups/${groupId}/groupMembers`)
.get()
const groupMemberDocs = groupMembersSnap.docs.map(
(doc) => doc.data() as { userId: string; createdTime: number }
)
if (
!groupMemberDocs.map((m) => m.userId).includes(user.id) &&
!group.anyoneCanJoin &&
group.creatorId !== user.id
) {
throw new APIError(
400,
'User must be a member/creator of the group or group must be open to add markets to it.'
)
}
}
const slug = await getSlug(question)
const contractRef = firestore.collection('contracts').doc()
console.log(
'creating contract for',
user.username,
'on',
question,
'ante:',
ante || 0
)
// convert string descriptions into JSONContent
const newDescription =
!description || typeof description === 'string'
? {
type: 'doc',
content: [
{
type: 'paragraph',
content: [{ type: 'text', text: description || ' ' }],
},
],
}
: description
const contract = getNewContract(
contractRef.id,
slug,
user,
question,
outcomeType,
newDescription,
initialProb ?? 0,
ante,
closeTime.getTime(),
tags ?? [],
NUMERIC_BUCKET_COUNT,
min ?? 0,
max ?? 0,
isLogScale ?? false,
answers ?? [],
visibility
)
const providerId = deservesFreeMarket
? isProd()
? HOUSE_LIQUIDITY_PROVIDER_ID
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
: user.id
if (ante) await chargeUser(providerId, ante, true)
if (deservesFreeMarket)
await firestore
.collection('users')
.doc(user.id)
.update({ freeMarketsCreated: FieldValue.increment(1) })
await contractRef.create(contract)
if (group != null) {
const groupContractsSnap = await firestore
.collection(`groups/${groupId}/groupContracts`)
.get()
const groupContracts = groupContractsSnap.docs.map(
(doc) => doc.data() as { contractId: string; createdTime: number }
)
if (!groupContracts.map((c) => c.contractId).includes(contractRef.id)) {
await createGroupLinks(group, [contractRef.id], auth.uid)
const groupContractRef = firestore
.collection(`groups/${groupId}/groupContracts`)
.doc(contract.id)
await groupContractRef.set({
contractId: contract.id,
createdTime: Date.now(),
})
}
}
if (outcomeType === 'BINARY' || outcomeType === 'PSEUDO_NUMERIC') {
const liquidityDoc = firestore
.collection(`contracts/${contract.id}/liquidity`)
.doc()
const lp = getCpmmInitialLiquidity(
providerId,
contract as CPMMBinaryContract,
liquidityDoc.id,
ante
)
await liquidityDoc.set(lp)
} else if (outcomeType === 'MULTIPLE_CHOICE') {
const betCol = firestore.collection(`contracts/${contract.id}/bets`)
const betDocs = (answers ?? []).map(() => betCol.doc())
const answerCol = firestore.collection(`contracts/${contract.id}/answers`)
const answerDocs = (answers ?? []).map((_, i) =>
answerCol.doc(i.toString())
)
const { bets, answerObjects } = getMultipleChoiceAntes(
user,
contract as MultipleChoiceContract,
answers ?? [],
betDocs.map((bd) => bd.id)
)
await Promise.all(
zip(bets, betDocs).map(([bet, doc]) => doc?.create(bet as Bet))
)
await Promise.all(
zip(answerObjects, answerDocs).map(([answer, doc]) =>
doc?.create(answer as Answer)
)
)
await contractRef.update({ answers: answerObjects })
} else if (outcomeType === 'FREE_RESPONSE') {
const noneAnswerDoc = firestore
.collection(`contracts/${contract.id}/answers`)
.doc('0')
const noneAnswer = getNoneAnswer(contract.id, user)
await noneAnswerDoc.set(noneAnswer)
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getFreeAnswerAnte(
providerId,
contract as FreeResponseContract,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
} else if (outcomeType === 'NUMERIC') {
const anteBetDoc = firestore
.collection(`contracts/${contract.id}/bets`)
.doc()
const anteBet = getNumericAnte(
providerId,
contract as NumericContract,
ante,
anteBetDoc.id
)
await anteBetDoc.set(anteBet)
}
return contract
}
const getSlug = async (question: string) => {
const proposedSlug = slugify(question)
const preexistingContract = await getContractFromSlug(proposedSlug)
return preexistingContract
? proposedSlug + '-' + randomString()
: proposedSlug
}
const firestore = admin.firestore()
export async function getContractFromSlug(slug: string) {
const snap = await firestore
.collection('contracts')
.where('slug', '==', slug)
.get()
return snap.empty ? undefined : (snap.docs[0].data() as Contract)
}
async function createGroupLinks(
group: Group,
contractIds: string[],
userId: string
) {
for (const contractId of contractIds) {
const contract = await getContract(contractId)
if (!contract?.groupSlugs?.includes(group.slug)) {
await firestore
.collection('contracts')
.doc(contractId)
.update({
groupSlugs: uniq([group.slug, ...(contract?.groupSlugs ?? [])]),
})
}
if (!contract?.groupLinks?.map((gl) => gl.groupId).includes(group.id)) {
await firestore
.collection('contracts')
.doc(contractId)
.update({
groupLinks: [
{
groupId: group.id,
name: group.name,
slug: group.slug,
userId,
createdTime: Date.now(),
} as GroupLink,
...(contract?.groupLinks ?? []),
],
})
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
import * as admin from 'firebase-admin'
import { getUser } from './utils'
import { slugify } from '../../common/util/slugify'
import { randomString } from '../../common/util/random'
import {
Post,
MAX_POST_TITLE_LENGTH,
MAX_POST_SUBTITLE_LENGTH,
} from '../../common/post'
import { APIError, newEndpoint, validate } from './api'
import { JSONContent } from '@tiptap/core'
import { z } from 'zod'
import { removeUndefinedProps } from '../../common/util/object'
import { createMarketHelper } from './create-market'
import { DAY_MS } from '../../common/util/time'
const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
z.intersection(
z.record(z.any()),
z.object({
type: z.string().optional(),
attrs: z.record(z.any()).optional(),
content: z.array(contentSchema).optional(),
marks: z
.array(
z.intersection(
z.record(z.any()),
z.object({
type: z.string(),
attrs: z.record(z.any()).optional(),
})
)
)
.optional(),
text: z.string().optional(),
})
)
)
const postSchema = z.object({
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
subtitle: z.string().min(1).max(MAX_POST_SUBTITLE_LENGTH),
content: contentSchema,
groupId: z.string().optional(),
// Date doc fields:
bounty: z.number().optional(),
birthday: z.number().optional(),
type: z.string().optional(),
question: z.string().optional(),
})
export const createpost = newEndpoint({}, async (req, auth) => {
const firestore = admin.firestore()
const { title, subtitle, content, groupId, question, ...otherProps } =
validate(postSchema, req.body)
const creator = await getUser(auth.uid)
if (!creator)
throw new APIError(400, 'No user exists with the authenticated user ID.')
console.log('creating post owned by', creator.username, 'titled', title)
const slug = await getSlug(title)
const postRef = firestore.collection('posts').doc()
// If this is a date doc, create a market for it.
let contractSlug
if (question) {
const closeTime = Date.now() + DAY_MS * 30 * 3
try {
const result = await createMarketHelper(
{
question,
closeTime,
outcomeType: 'BINARY',
visibility: 'unlisted',
initialProb: 50,
// Dating group!
groupId: 'j3ZE8fkeqiKmRGumy3O1',
},
auth
)
contractSlug = result.slug
} catch (e) {
console.error(e)
}
}
const post: Post = removeUndefinedProps({
...otherProps,
id: postRef.id,
creatorId: creator.id,
slug,
title,
subtitle,
createdTime: Date.now(),
content: content,
contractSlug,
creatorName: creator.name,
creatorUsername: creator.username,
creatorAvatarUrl: creator.avatarUrl,
itemType: 'post',
})
await postRef.create(post)
if (groupId) {
const groupRef = firestore.collection('groups').doc(groupId)
const group = await groupRef.get()
if (group.exists) {
const groupData = group.data()
if (groupData) {
const postIds = groupData.postIds ?? []
postIds.push(postRef.id)
await groupRef.update({ postIds })
}
}
}
return { status: 'success', post }
})
export const getSlug = async (title: string) => {
const proposedSlug = slugify(title)
const preexistingPost = await getPostFromSlug(proposedSlug)
return preexistingPost ? proposedSlug + '-' + randomString() : proposedSlug
}
export async function getPostFromSlug(slug: string) {
const firestore = admin.firestore()
const snap = await firestore
.collection('posts')
.where('slug', '==', slug)
.get()
return snap.empty ? undefined : (snap.docs[0].data() as Post)
}

View File

@ -1,20 +1,13 @@
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { z } from 'zod' import { z } from 'zod'
import {
MANIFOLD_AVATAR_URL, import { PrivateUser, User } from '../../common/user'
MANIFOLD_USERNAME, import { getUser, getUserByUsername, getValues } from './utils'
PrivateUser,
STARTING_BALANCE,
SUS_STARTING_BALANCE,
User,
} from '../../common/user'
import { getUser, getUserByUsername, getValues, isProd } from './utils'
import { randomString } from '../../common/util/random' import { randomString } from '../../common/util/random'
import { import {
cleanDisplayName, cleanDisplayName,
cleanUsername, cleanUsername,
} from '../../common/util/clean-username' } from '../../common/util/clean-username'
import { sendWelcomeEmail } from './emails'
import { isWhitelisted } from '../../common/envs/constants' import { isWhitelisted } from '../../common/envs/constants'
import { import {
CATEGORIES_GROUP_SLUG_POSTFIX, CATEGORIES_GROUP_SLUG_POSTFIX,
@ -23,12 +16,9 @@ import {
import { track } from './analytics' import { track } from './analytics'
import { APIError, newEndpoint, validate } from './api' import { APIError, newEndpoint, validate } from './api'
import { Group, NEW_USER_GROUP_SLUGS } from '../../common/group' import { Group } from '../../common/group'
import { uniq } from 'lodash' import { SUS_STARTING_BALANCE, STARTING_BALANCE } from '../../common/economy'
import { import { getDefaultNotificationPreferences } from '../../common/user-notification-preferences'
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
HOUSE_LIQUIDITY_PROVIDER_ID,
} from '../../common/antes'
const bodySchema = z.object({ const bodySchema = z.object({
deviceToken: z.string().optional(), deviceToken: z.string().optional(),
@ -63,10 +53,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
const deviceUsedBefore = const deviceUsedBefore =
!deviceToken || (await isPrivateUserWithDeviceToken(deviceToken)) !deviceToken || (await isPrivateUserWithDeviceToken(deviceToken))
const ipCount = req.ip ? await numberUsersWithIp(req.ip) : 0 const balance = deviceUsedBefore ? SUS_STARTING_BALANCE : STARTING_BALANCE
const balance =
deviceUsedBefore || ipCount > 2 ? SUS_STARTING_BALANCE : STARTING_BALANCE
const user: User = { const user: User = {
id: auth.uid, id: auth.uid,
@ -78,8 +65,12 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
createdTime: Date.now(), createdTime: Date.now(),
profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
nextLoanCached: 0,
followerCountCached: 0, followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES, followedCategories: DEFAULT_CATEGORIES,
shouldShowWelcome: true,
fractionResolvedCorrectly: 1,
achievements: {},
} }
await firestore.collection('users').doc(auth.uid).create(user) await firestore.collection('users').doc(auth.uid).create(user)
@ -91,15 +82,15 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
email, email,
initialIpAddress: req.ip, initialIpAddress: req.ip,
initialDeviceToken: deviceToken, initialDeviceToken: deviceToken,
notificationPreferences: getDefaultNotificationPreferences(auth.uid),
} }
await firestore.collection('private-users').doc(auth.uid).create(privateUser) await firestore.collection('private-users').doc(auth.uid).create(privateUser)
await sendWelcomeEmail(user, privateUser)
await addUserToDefaultGroups(user) await addUserToDefaultGroups(user)
await track(auth.uid, 'create user', { username }, { ip: req.ip }) await track(auth.uid, 'create user', { username }, { ip: req.ip })
return user return { user, privateUser }
}) })
const firestore = admin.firestore() const firestore = admin.firestore()
@ -113,7 +104,7 @@ const isPrivateUserWithDeviceToken = async (deviceToken: string) => {
return !snap.empty return !snap.empty
} }
const numberUsersWithIp = async (ipAddress: string) => { export const numberUsersWithIp = async (ipAddress: string) => {
const snap = await firestore const snap = await firestore
.collection('private-users') .collection('private-users')
.where('initialIpAddress', '==', ipAddress) .where('initialIpAddress', '==', ipAddress)
@ -129,42 +120,8 @@ const addUserToDefaultGroups = async (user: User) => {
firestore.collection('groups').where('slug', '==', slug) firestore.collection('groups').where('slug', '==', slug)
) )
await firestore await firestore
.collection('groups') .collection(`groups/${groups[0].id}/groupMembers`)
.doc(groups[0].id) .doc(user.id)
.update({ .set({ userId: user.id, createdTime: Date.now() })
memberIds: uniq(groups[0].memberIds.concat(user.id)),
})
}
for (const slug of NEW_USER_GROUP_SLUGS) {
const groups = await getValues<Group>(
firestore.collection('groups').where('slug', '==', slug)
)
const group = groups[0]
await firestore
.collection('groups')
.doc(group.id)
.update({
memberIds: uniq(group.memberIds.concat(user.id)),
})
const manifoldAccount = isProd()
? HOUSE_LIQUIDITY_PROVIDER_ID
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
if (slug === 'welcome') {
const welcomeCommentDoc = firestore
.collection(`groups/${group.id}/comments`)
.doc()
await welcomeCommentDoc.create({
id: welcomeCommentDoc.id,
groupId: group.id,
userId: manifoldAccount,
text: `Welcome, ${user.name} (@${user.username})!`,
createdTime: Date.now(),
userName: 'Manifold Markets',
userUsername: MANIFOLD_USERNAME,
userAvatarUrl: MANIFOLD_AVATAR_URL,
})
}
} }
} }

View File

@ -0,0 +1,69 @@
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import { CPMMContract } from '../../common/contract'
import { batchedWaitAll } from '../../common/util/promise'
import { APIError } from '../../common/api'
import { addCpmmLiquidity } from '../../common/calculate-cpmm'
import { formatMoneyWithDecimals } from '../../common/util/format'
const firestore = admin.firestore()
export const drizzleLiquidity = async () => {
const snap = await firestore
.collection('contracts')
.where('subsidyPool', '>', 1e-7)
.get()
const contractIds = snap.docs.map((doc) => doc.id)
console.log('found', contractIds.length, 'markets to drizzle')
console.log()
await batchedWaitAll(
contractIds.map((cid) => () => drizzleMarket(cid)),
10
)
}
export const drizzleLiquidityScheduler = functions.pubsub
.schedule('* * * * *') // every minute
.onRun(drizzleLiquidity)
const drizzleMarket = async (contractId: string) => {
await firestore.runTransaction(async (trans) => {
const snap = await trans.get(firestore.doc(`contracts/${contractId}`))
const contract = snap.data() as CPMMContract
const { subsidyPool, pool, p, slug, popularityScore } = contract
if ((subsidyPool ?? 0) < 1e-7) return
const r = Math.random()
const logPopularity = Math.log10((popularityScore ?? 0) + 1)
const v = Math.max(1, Math.min(5, logPopularity))
const amount = subsidyPool <= 0.5 ? subsidyPool : r * v * 0.01 * subsidyPool
const { newPool, newP } = addCpmmLiquidity(pool, p, amount)
if (!isFinite(newP)) {
throw new APIError(
500,
'Liquidity injection rejected due to overflow error.'
)
}
await trans.update(firestore.doc(`contracts/${contract.id}`), {
pool: newPool,
p: newP,
subsidyPool: subsidyPool - amount,
})
console.log(
'added subsidy',
formatMoneyWithDecimals(amount),
'of',
formatMoneyWithDecimals(subsidyPool),
'pool to',
slug
)
console.log()
})
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,526 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Manifold Market Creation Guide</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto" src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top"
width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Hi {{name}},</span></p>
</div>
</td>
</tr>
<tr>
<td align="left" style="
font-size: 0px;
padding: 0px 25px 20px 25px;
padding-top: 0px;
padding-right: 25px;
padding-bottom: 20px;
padding-left: 25px;
word-break: break-word;
">
<div style="
font-family: Arial, sans-serif;
font-size: 17px;
letter-spacing: normal;
line-height: 1;
text-align: left;
color: #000000;
">
<p class="text-build-content" style="
line-height: 23px;
margin: 10px 0;
margin-top: 10px;
" data-testid="3Q8BP69fq">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Did you know you can create your own prediction market on <a
class="link-build-content" style="color: #55575d" target="_blank"
href="https://manifold.markets">Manifold</a> on
any question you care about?</span>
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Whether it's current events like <a class="link-build-content" style="color: #55575d"
target="_blank"
href="https://manifold.markets/SG/will-elon-musk-buy-twitter-this-yea">Musk buying
Twitter</a> or <a class="link-build-content" style="color: #55575d" target="_blank"
href="https://manifold.markets/NathanpmYoung/will-biden-be-the-2024-democratic-n">2024
elections</a> or personal matters
like <a class="link-build-content" style="color: #55575d" target="_blank"
href="https://manifold.markets/dreev/which-book-will-i-like-best">book
recommendations</a> or <a class="link-build-content" style="color: #55575d"
target="_blank"
href="https://manifold.markets/agentydragon/will-my-weight-go-under-115-kg-in-2">losing
weight</a>,
Manifold can help you find the answer.</span>
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0;margin-bottom: 20px;"
data-testid="3Q8BP69fq">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">The following is a
short guide to creating markets.</span>
</p>
<table cellspacing="0" cellpadding="0" align="center">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="https://manifold.markets/create" target="_blank"
style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
Create a market
</a>
</td>
</tr>
</table>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
&nbsp;
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
color: #292fd7;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 20px;
"><b>What makes a good market?</b></span>
</p>
<ul>
<li style="line-height: 23px; margin-bottom: 8px;">
<span
style="font-family: Readex Pro, Arial, Helvetica, sans-serif;font-size: 17px;"><b>Interesting
topic. </b>Manifold gives
creators M$10 for
each unique trader that bets on your
market, so it pays to ask a question people are interested in!</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Clear resolution criteria. </b>Any ambiguities or edge cases in your description
will drive traders away from your markets.</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Detailed description. </b>Include images/videos/tweets and any context or
background
information that could be useful to people who
are interested in learning more that are
uneducated on the subject.</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Part of a group. </b>Groups are the
primary way users filter for relevant markets.
Also, consider making your own groups and
inviting friends/interested communities to
them from other sites!</span>
</li>
<li style="line-height: 23px; margin-bottom: 8px;">
<span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><b>Sharing it on social media</b>. You'll earn the <a class="link-build-content"
style="color: inherit; text-decoration: none" target="_blank"
href="https://manifold.markets/referrals"><span style="
color: #55575d;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><u>M$500
referral bonus</u></span></a> if you get new users to sign up!</span>
</li>
</ul>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
&nbsp;
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
color: #000000;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Why not </span>
<a class="link-build-content" style="color: inherit; text-decoration: none" target="_blank"
href="https://manifold.markets/create"><span style="
color: #55575d;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
"><u>create a market</u></span></a><span style="
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">
while it is still fresh on your mind?
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
&nbsp;
</p>
<p class="text-build-content" style="line-height: 23px; margin: 10px 0" data-testid="3Q8BP69fq">
<span style="
color: #000000;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">Thanks for reading!</span>
</p>
<p class="text-build-content" style="
line-height: 23px;
margin: 10px 0;
margin-bottom: 10px;
" data-testid="3Q8BP69fq">
<span style="
color: #000000;
font-family: Readex Pro, Arial, Helvetica,
sans-serif;
font-size: 17px;
">David from Manifold</span>
</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<div style="background-color: #f4f4f4">
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top"
width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 0px 25px 0px 25px;
padding-top: 0px;
padding-right: 25px;
padding-bottom: 0px;
padding-left: 25px;
word-break: break-word;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="
border-collapse: collapse;
border-spacing: 0px;
">
<tbody>
<tr>
<td style="width: 550px">
<a href="https://manifold.markets/create" target="_blank"><img alt="" height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/96u/omk8.gif" style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
" width="550" /></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to {{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -0,0 +1,475 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>Interesting markets on Manifold</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto"
src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align: top" width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Here is a selection of markets on Manifold you might find
interesting!</span></p>
</div>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question1Link}}">
<img alt="{{question1Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question1ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question1Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question2Link}}">
<img alt="{{question2Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question2ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question2Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question3Link}}">
<img alt="{{question3Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question3ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question3Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question4Link}}">
<img alt="{{question4Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question4ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question4Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question5Link}}">
<img alt="{{question5Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question5ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question5Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{question6Link}}">
<img alt="{{question6Title}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{question6ImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{question6Link}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to
{{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</div>
</body>
</html>

View File

@ -526,19 +526,10 @@
" "
>our Discord</a >our Discord</a
>! Or, >! Or,
<a <a href="{{unsubscribeUrl}}" style="
href="{{unsubscribeUrl}}" color: inherit;
style=" text-decoration: none;
font-family: 'Helvetica Neue', Helvetica, Arial, " target="_blank">click here to unsubscribe from this type of notification</a>.
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td> </td>
</tr> </tr>
</table> </table>

View File

@ -1,84 +1,91 @@
<!DOCTYPE html> <!DOCTYPE html>
<html <html style="
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market answer</title>
<style type="text/css"> <head>
img { <meta name="viewport" content="width=device-width" />
max-width: 100%; <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market answer</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
} }
body { h1 {
-webkit-font-smoothing: antialiased; font-weight: 800 !important;
-webkit-text-size-adjust: none; margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important; width: 100% !important;
height: 100%;
line-height: 1.6em;
} }
body { .content {
background-color: #f6f6f6; padding: 0 !important;
} }
@media only screen and (max-width: 640px) { .content-wrap {
body { padding: 10px !important;
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
} }
</style>
</head>
<body .invoice {
itemscope width: 100% !important;
itemtype="http://schema.org/EmailMessage" }
style=" }
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -89,43 +96,29 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <table class="body-wrap" style="
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" <td class="container" width="600" style="
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -134,12 +127,8 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" " valign="top">
valign="top" <div class="content" style="
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -147,14 +136,8 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
" ">
> <table class="main" width="100%" cellpadding="0" cellspacing="0" style="
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -162,20 +145,14 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" " bgcolor="#fff">
bgcolor="#fff" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-wrap aligncenter" style="
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -183,35 +160,23 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" " align="center" valign="top">
align="center" <table width="100%" cellpadding="0" cellspacing="0" style="
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -220,29 +185,21 @@
margin: 0; margin: 0;
padding: 0 0 0px 0; padding: 0 0 0px 0;
text-align: left; text-align: left;
" " valign="top">
valign="top" <a href="https://manifold.markets" target="_blank">
> <img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
<img alt="Manifold Markets" />
src="https://manifold.markets/logo-banner.png" </a>
width="300" </td>
style="height: auto" </tr>
alt="Manifold Markets" <tr style="
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td class="content-block aligncenter" style="
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -251,13 +208,8 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" " align="center" valign="top">
align="center" <table class="invoice" style="
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -266,19 +218,15 @@
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
margin-top: 10px; margin-top: 10px;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -286,37 +234,26 @@
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
font-weight: bold; font-weight: bold;
" " valign="top">
valign="top" <div>
> <img src="{{avatarUrl}}" width="30" height="30" style="
<div>
<img
src="{{avatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px; border-radius: 30px;
overflow: hidden; overflow: hidden;
vertical-align: middle; vertical-align: middle;
margin-right: 4px; margin-right: 4px;
" " alt="" />
alt="" {{name}}
/> </div>
{{name}} </td>
</div> </tr>
</td> <tr style="
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -324,40 +261,29 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" " valign="top">
valign="top" <div style="
>
<div
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <span style="white-space: pre-line">{{answer}}</span>
<span style="white-space: pre-line" </div>
>{{answer}}</span </td>
> </tr>
</div> <tr style="
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="padding: 20px 0 0 0; margin: 0">
<td style="padding: 20px 0 0 0; margin: 0"> <div align="center">
<div align="center"> <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]--> <a href="{{marketUrl}}" target="_blank" style="
<a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -375,38 +301,29 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
" ">
> <span style="
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
" "><span style="
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
" ">View answer</span></span>
>View answer</span </a>
></span <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
> </div>
</a> </td>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> </tr>
</div> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> <div class="footer" style="
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -415,28 +332,20 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" ">
> <table width="100%" style="
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="aligncenter content-block" style="
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -446,14 +355,9 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" " align="center" valign="top">
align="center" Questions? Come ask in
valign="top" <a href="https://discord.gg/eHQBNBqXuh" style="
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -461,39 +365,26 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
" ">our Discord</a>! Or,
>our Discord</a <a href="{{unsubscribeUrl}}" style="
>! Or, color: inherit;
<a text-decoration: none;
href="{{unsubscribeUrl}}" " target="_blank">click here to unsubscribe from this type of notification</a>.
style=" </td>
font-family: 'Helvetica Neue', Helvetica, Arial, </tr>
sans-serif; </table>
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div> </div>
</td> </div>
<td </td>
style=" <td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" </tr>
></td> </table>
</tr> </body>
</table>
</body> </html>
</html>

View File

@ -1,84 +1,91 @@
<!DOCTYPE html> <!DOCTYPE html>
<html <html style="
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market closed</title>
<style type="text/css"> <head>
img { <meta name="viewport" content="width=device-width" />
max-width: 100%; <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market closed</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
} }
body { h1 {
-webkit-font-smoothing: antialiased; font-weight: 800 !important;
-webkit-text-size-adjust: none; margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important; width: 100% !important;
height: 100%;
line-height: 1.6em;
} }
body { .content {
background-color: #f6f6f6; padding: 0 !important;
} }
@media only screen and (max-width: 640px) { .content-wrap {
body { padding: 10px !important;
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
} }
</style>
</head>
<body .invoice {
itemscope width: 100% !important;
itemtype="http://schema.org/EmailMessage" }
style=" }
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -89,43 +96,29 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <table class="body-wrap" style="
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" <td class="container" width="600" style="
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -134,12 +127,8 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" " valign="top">
valign="top" <div class="content" style="
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -147,14 +136,8 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
" ">
> <table class="main" width="100%" cellpadding="0" cellspacing="0" style="
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -162,20 +145,14 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" " bgcolor="#fff">
bgcolor="#fff" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-wrap aligncenter" style="
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -183,35 +160,23 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" " align="center" valign="top">
align="center" <table width="100%" cellpadding="0" cellspacing="0" style="
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -220,30 +185,22 @@
margin: 0; margin: 0;
padding: 0 0 40px 0; padding: 0 0 40px 0;
text-align: left; text-align: left;
" " valign="top">
valign="top" <a href="https://manifold.markets" target="_blank">
> <img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
<img alt="Manifold Markets" />
src="https://manifold.markets/logo-banner.png" </a>
width="300" </td>
style="height: auto" </tr>
alt="Manifold Markets" <tr style="
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
padding: 0; padding: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -252,24 +209,18 @@
margin: 0; margin: 0;
padding: 0 0 6px 0; padding: 0 0 6px 0;
text-align: left; text-align: left;
" " valign="top">
valign="top" You asked
> </td>
You asked </tr>
</td> <tr style="
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -277,12 +228,8 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" " valign="top">
valign="top" <a href="{{url}}" style="
>
<a
href="{{url}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -295,24 +242,18 @@
color: #4337c9; color: #4337c9;
display: block; display: block;
text-decoration: none; text-decoration: none;
" ">
> {{question}}</a>
{{question}}</a </td>
> </tr>
</td> <tr style="
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -320,12 +261,8 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 0px; padding: 0 0 0px;
" " valign="top">
valign="top" <h2 class="aligncenter" style="
>
<h2
class="aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -335,25 +272,19 @@
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
margin: 10px 0 0; margin: 10px 0 0;
" " align="center">
align="center" Market closed
> </h2>
Market closed </td>
</h2> </tr>
</td> <tr style="
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td class="content-block aligncenter" style="
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -362,13 +293,8 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" " align="center" valign="top">
align="center" <table class="invoice" style="
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -376,19 +302,15 @@
text-align: left; text-align: left;
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -396,116 +318,90 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" " valign="top">
valign="top" Hi {{name}},
> <br style="
Hi {{name}},
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> A market you created has closed. It's attracted
A market you created has closed. It's attracted <span style="font-weight: bold">{{volume}}</span>
<span style="font-weight: bold">{{volume}}</span> in bets — congrats!
in bets — congrats! <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> Please resolve your market.
Resolve your market to earn {{creatorFee}} as the <br style="
creator commission.
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> Thanks,
Thanks, <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> Manifold Team
Manifold Team <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> </td>
</td> </tr>
</tr> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="padding: 10px 0 0 0; margin: 0">
<td style="padding: 10px 0 0 0; margin: 0"> <div align="center">
<div align="center"> <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]--> <a href="{{url}}" target="_blank" style="
<a
href="{{url}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -523,38 +419,29 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
" ">
> <span style="
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
" "><span style="
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
" ">View market</span></span>
>View market</span </a>
></span <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
> </div>
</a> </td>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> </tr>
</div> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> <div class="footer" style="
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -563,28 +450,20 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" ">
> <table width="100%" style="
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="aligncenter content-block" style="
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -594,14 +473,9 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" " align="center" valign="top">
align="center" Questions? Come ask in
valign="top" <a href="https://discord.gg/eHQBNBqXuh" style="
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -609,39 +483,22 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
" ">our Discord</a>!
>our Discord</a </td>
>! Or, </tr>
<a </table>
href="{{unsubscribeUrl}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div> </div>
</td> </div>
<td </td>
style=" <td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" </tr>
></td> </table>
</tr> </body>
</table>
</body> </html>
</html>

View File

@ -1,84 +1,91 @@
<!DOCTYPE html> <!DOCTYPE html>
<html <html style="
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market comment</title>
<style type="text/css"> <head>
img { <meta name="viewport" content="width=device-width" />
max-width: 100%; <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market comment</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
} }
body { h1 {
-webkit-font-smoothing: antialiased; font-weight: 800 !important;
-webkit-text-size-adjust: none; margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important; width: 100% !important;
height: 100%;
line-height: 1.6em;
} }
body { .content {
background-color: #f6f6f6; padding: 0 !important;
} }
@media only screen and (max-width: 640px) { .content-wrap {
body { padding: 10px !important;
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
} }
</style>
</head>
<body .invoice {
itemscope width: 100% !important;
itemtype="http://schema.org/EmailMessage" }
style=" }
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -89,43 +96,29 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <table class="body-wrap" style="
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" <td class="container" width="600" style="
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -134,12 +127,8 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" " valign="top">
valign="top" <div class="content" style="
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -147,14 +136,8 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
" ">
> <table class="main" width="100%" cellpadding="0" cellspacing="0" style="
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -162,20 +145,14 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" " bgcolor="#fff">
bgcolor="#fff" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-wrap aligncenter" style="
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -183,35 +160,23 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" " align="center" valign="top">
align="center" <table width="100%" cellpadding="0" cellspacing="0" style="
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -220,29 +185,21 @@
margin: 0; margin: 0;
padding: 0 0 0px 0; padding: 0 0 0px 0;
text-align: left; text-align: left;
" " valign="top">
valign="top" <a href="https://manifold.markets" target="_blank">
> <img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
<img alt="Manifold Markets" />
src="https://manifold.markets/logo-banner.png" </a>
width="300" </td>
style="height: auto" </tr>
alt="Manifold Markets" <tr style="
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td class="content-block aligncenter" style="
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -251,13 +208,8 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" " align="center" valign="top">
align="center" <table class="invoice" style="
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -266,59 +218,42 @@
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
margin-top: 10px; margin-top: 10px;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" " valign="top">
valign="top" <div>
> <img src="{{commentorAvatarUrl}}" width="30" height="30" style="
<div>
<img
src="{{commentorAvatarUrl}}"
width="30"
height="30"
style="
border-radius: 30px; border-radius: 30px;
overflow: hidden; overflow: hidden;
vertical-align: middle; vertical-align: middle;
margin-right: 4px; margin-right: 4px;
" " alt="" />
alt="" <span style="font-weight: bold">{{commentorName}}</span>
/> {{betDescription}}
<span style="font-weight: bold" </div>
>{{commentorName}}</span </td>
> </tr>
{{betDescription}} <tr style="
</div>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -326,40 +261,29 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" " valign="top">
valign="top" <div style="
>
<div
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <span style="white-space: pre-line">{{comment}}</span>
<span style="white-space: pre-line" </div>
>{{comment}}</span </td>
> </tr>
</div> <tr style="
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="padding: 20px 0 0 0; margin: 0">
<td style="padding: 20px 0 0 0; margin: 0"> <div align="center">
<div align="center"> <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]--> <a href="{{marketUrl}}" target="_blank" style="
<a
href="{{marketUrl}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -377,38 +301,29 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
" ">
> <span style="
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
" "><span style="
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
" ">View comment</span></span>
>View comment</span </a>
></span <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
> </div>
</a> </td>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> </tr>
</div> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> <div class="footer" style="
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -417,28 +332,20 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" ">
> <table width="100%" style="
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="aligncenter content-block" style="
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -448,14 +355,9 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" " align="center" valign="top">
align="center" Questions? Come ask in
valign="top" <a href="https://discord.gg/eHQBNBqXuh" style="
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -463,39 +365,26 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
" ">our Discord</a>! Or,
>our Discord</a <a href="{{unsubscribeUrl}}" style="
>! Or, color: inherit;
<a text-decoration: none;
href="{{unsubscribeUrl}}" " target="_blank">click here to unsubscribe from this type of notification</a>.
style=" </td>
font-family: 'Helvetica Neue', Helvetica, Arial, </tr>
sans-serif; </table>
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div> </div>
</td> </div>
<td </td>
style=" <td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" </tr>
></td> </table>
</tr> </body>
</table>
</body> </html>
</html>

View File

@ -0,0 +1,491 @@
<!DOCTYPE html>
<html style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market resolved</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
" bgcolor="#f6f6f6">
<table class="body-wrap" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
" bgcolor="#f6f6f6">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
" valign="top"></td>
<td class="container" width="600" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
" valign="top">
<div class="content" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
">
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
" bgcolor="#fff">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-wrap aligncenter" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
" align="center" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 40px 0;
text-align: left;
" valign="top">
<a href="https://manifold.markets" target="_blank">
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
alt="Manifold Markets" />
</a>
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 0;
">
<td class="content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 6px 0;
text-align: left;
" valign="top">
{{creatorName}} asked
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
padding: 0 0 20px;
" valign="top">
<a href="{{url}}" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif;
box-sizing: border-box;
font-size: 24px;
color: #000;
line-height: 1.2em;
font-weight: 500;
text-align: left;
margin: 0 0 0 0;
color: #4337c9;
display: block;
text-decoration: none;
">
{{question}}</a>
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
padding: 0 0 0px;
" valign="top">
<h2 class="aligncenter" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif;
box-sizing: border-box;
font-size: 24px;
color: #000;
line-height: 1.2em;
font-weight: 500;
text-align: center;
margin: 10px 0 0;
" align="center">
Resolved {{outcome}}
</h2>
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
">
<td class="content-block aligncenter" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
" align="center" valign="top">
<table class="invoice" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 5px 0;
" valign="top">
Dear {{name}},
<br style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
" />
<br style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
" />
A market you were following has been resolved!
<br style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
" />
<br style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
" />
Thanks,
<br style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
" />
Manifold Team
<br style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
" />
<br style="
font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
" />
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
">
<td style="padding: 10px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a href="{{url}}" target="_blank" style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
">
<span style="
display: block;
padding: 10px 20px;
line-height: 120%;
"><span style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
">View market</span></span>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div class="footer" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
">
<table width="100%" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="aligncenter content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
" align="center" valign="top">
Questions? Come ask in
<a href="https://discord.gg/eHQBNBqXuh" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
">our Discord</a>! Or,
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
" valign="top"></td>
</tr>
</table>
</body>
</html>

View File

@ -1,84 +1,91 @@
<!DOCTYPE html> <!DOCTYPE html>
<html <html style="
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market resolved</title>
<style type="text/css"> <head>
img { <meta name="viewport" content="width=device-width" />
max-width: 100%; <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Market resolved</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
} }
body { h1 {
-webkit-font-smoothing: antialiased; font-weight: 800 !important;
-webkit-text-size-adjust: none; margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important; width: 100% !important;
height: 100%;
line-height: 1.6em;
} }
body { .content {
background-color: #f6f6f6; padding: 0 !important;
} }
@media only screen and (max-width: 640px) { .content-wrap {
body { padding: 10px !important;
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
} }
</style>
</head>
<body .invoice {
itemscope width: 100% !important;
itemtype="http://schema.org/EmailMessage" }
style=" }
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -89,43 +96,29 @@
line-height: 1.6em; line-height: 1.6em;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <table class="body-wrap" style="
>
<table
class="body-wrap"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
width: 100%; width: 100%;
background-color: #f6f6f6; background-color: #f6f6f6;
margin: 0; margin: 0;
" " bgcolor="#f6f6f6">
bgcolor="#f6f6f6" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" <td class="container" width="600" style="
></td>
<td
class="container"
width="600"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -134,12 +127,8 @@
max-width: 600px !important; max-width: 600px !important;
clear: both !important; clear: both !important;
margin: 0 auto; margin: 0 auto;
" " valign="top">
valign="top" <div class="content" style="
>
<div
class="content"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -147,14 +136,8 @@
display: block; display: block;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
" ">
> <table class="main" width="100%" cellpadding="0" cellspacing="0" style="
<table
class="main"
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -162,20 +145,14 @@
background-color: #fff; background-color: #fff;
margin: 0; margin: 0;
border: 1px solid #e9e9e9; border: 1px solid #e9e9e9;
" " bgcolor="#fff">
bgcolor="#fff" <tr style="
>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-wrap aligncenter" style="
<td
class="content-wrap aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -183,35 +160,23 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" " align="center" valign="top">
align="center" <table width="100%" cellpadding="0" cellspacing="0" style="
valign="top"
>
<table
width="100%"
cellpadding="0"
cellspacing="0"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
width: 90%; width: 90%;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -220,30 +185,22 @@
margin: 0; margin: 0;
padding: 0 0 40px 0; padding: 0 0 40px 0;
text-align: left; text-align: left;
" " valign="top">
valign="top" <a href="https://manifold.markets" target="_blank">
> <img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
<img alt="Manifold Markets" />
src="https://manifold.markets/logo-banner.png" </a>
width="300" </td>
style="height: auto" </tr>
alt="Manifold Markets" <tr style="
/>
</td>
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
padding: 0; padding: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -252,24 +209,18 @@
margin: 0; margin: 0;
padding: 0 0 6px 0; padding: 0 0 6px 0;
text-align: left; text-align: left;
" " valign="top">
valign="top" {{creatorName}} asked
> </td>
{{creatorName}} asked </tr>
</td> <tr style="
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -277,12 +228,8 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" " valign="top">
valign="top" <a href="{{url}}" style="
>
<a
href="{{url}}"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -295,24 +242,18 @@
color: #4337c9; color: #4337c9;
display: block; display: block;
text-decoration: none; text-decoration: none;
" ">
> {{question}}</a>
{{question}}</a </td>
> </tr>
</td> <tr style="
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="content-block" style="
<td
class="content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -320,12 +261,8 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 0 0 0px; padding: 0 0 0px;
" " valign="top">
valign="top" <h2 class="aligncenter" style="
>
<h2
class="aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
'Lucida Grande', sans-serif; 'Lucida Grande', sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -335,25 +272,19 @@
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
margin: 10px 0 0; margin: 10px 0 0;
" " align="center">
align="center" Resolved {{outcome}}
> </h2>
Resolved {{outcome}} </td>
</h2> </tr>
</td> <tr style="
</tr>
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td class="content-block aligncenter" style="
<td
class="content-block aligncenter"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -362,13 +293,8 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0; padding: 0;
" " align="center" valign="top">
align="center" <table class="invoice" style="
valign="top"
>
<table
class="invoice"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -376,19 +302,15 @@
text-align: left; text-align: left;
width: 80%; width: 80%;
margin: 40px auto; margin: 40px auto;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="
<td
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -396,138 +318,105 @@
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
padding: 5px 0; padding: 5px 0;
" " valign="top">
valign="top" Dear {{name}},
> <br style="
Dear {{name}},
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> A market you bet in has been resolved!
A market you bet in has been resolved! <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> Your investment was
Your investment was <span style="font-weight: bold">{{investment}}</span>.
<span style="font-weight: bold" <br style="
>M$ {{investment}}</span
>.
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> Your payout is
Your payout is <span style="font-weight: bold">{{payout}}</span>.
<span style="font-weight: bold" <br style="
>M$ {{payout}}</span
>.
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> Thanks,
Thanks, <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> Manifold Team
Manifold Team <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> <br style="
<br
style="
font-family: 'Helvetica Neue', Helvetica, font-family: 'Helvetica Neue', Helvetica,
Arial, sans-serif; Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" " />
/> </td>
</td> </tr>
</tr> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 16px;
margin: 0; margin: 0;
" ">
> <td style="padding: 10px 0 0 0; margin: 0">
<td style="padding: 10px 0 0 0; margin: 0"> <div align="center">
<div align="center"> <!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]--> <a href="{{url}}" target="_blank" style="
<a
href="{{url}}"
target="_blank"
style="
box-sizing: border-box; box-sizing: border-box;
display: inline-block; display: inline-block;
font-family: arial, helvetica, sans-serif; font-family: arial, helvetica, sans-serif;
@ -545,38 +434,29 @@
word-break: break-word; word-break: break-word;
word-wrap: break-word; word-wrap: break-word;
mso-border-alt: none; mso-border-alt: none;
" ">
> <span style="
<span
style="
display: block; display: block;
padding: 10px 20px; padding: 10px 20px;
line-height: 120%; line-height: 120%;
" "><span style="
><span
style="
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
line-height: 18.8px; line-height: 18.8px;
" ">View market</span></span>
>View market</span </a>
></span <!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
> </div>
</a> </td>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]--> </tr>
</div> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> </td>
</tr> </tr>
</table> </table>
</td> <div class="footer" style="
</tr>
</table>
<div
class="footer"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
@ -585,28 +465,20 @@
color: #999; color: #999;
margin: 0; margin: 0;
padding: 20px; padding: 20px;
" ">
> <table width="100%" style="
<table
width="100%"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <tr style="
<tr
style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
margin: 0; margin: 0;
" ">
> <td class="aligncenter content-block" style="
<td
class="aligncenter content-block"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -616,14 +488,9 @@
text-align: center; text-align: center;
margin: 0; margin: 0;
padding: 0 0 20px; padding: 0 0 20px;
" " align="center" valign="top">
align="center" Questions? Come ask in
valign="top" <a href="https://discord.gg/eHQBNBqXuh" style="
>
Questions? Come ask in
<a
href="https://discord.gg/eHQBNBqXuh"
style="
font-family: 'Helvetica Neue', Helvetica, Arial, font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif; sans-serif;
box-sizing: border-box; box-sizing: border-box;
@ -631,39 +498,26 @@
color: #999; color: #999;
text-decoration: underline; text-decoration: underline;
margin: 0; margin: 0;
" ">our Discord</a>! Or,
>our Discord</a <a href="{{unsubscribeUrl}}" style="
>! Or, color: inherit;
<a text-decoration: none;
href="{{unsubscribeUrl}}" " target="_blank">click here to unsubscribe from this type of notification</a>.
style=" </td>
font-family: 'Helvetica Neue', Helvetica, Arial, </tr>
sans-serif; </table>
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
"
>unsubscribe</a
>.
</td>
</tr>
</table>
</div>
</div> </div>
</td> </div>
<td </td>
style=" <td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box; box-sizing: border-box;
font-size: 14px; font-size: 14px;
vertical-align: top; vertical-align: top;
margin: 0; margin: 0;
" " valign="top"></td>
valign="top" </tr>
></td> </table>
</tr> </body>
</table>
</body> </html>
</html>

View File

@ -0,0 +1,354 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office">
<head>
<title>New market from {{creatorName}}</title>
<!--[if !mso]><!-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!--<![endif]-->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a {
padding: 0;
}
body {
margin: 0;
padding: 0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
table,
td {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
img {
border: 0;
height: auto;
line-height: 100%;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
p {
display: block;
margin: 13px 0;
}
</style>
<!--[if mso]>
<noscript>
<xml>
<o:OfficeDocumentSettings>
<o:AllowPNG />
<o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings>
</xml>
</noscript>
<![endif]-->
<!--[if lte mso 11]>
<style type="text/css">
.mj-outlook-group-fix {
width: 100% !important;
}
</style>
<![endif]-->
<!--[if !mso]><!-->
<link href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<link href="https://fonts.googleapis.com/css?family=Readex+Pro" rel="stylesheet" type="text/css" />
<style type="text/css">
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
@import url(https://fonts.googleapis.com/css?family=Readex+Pro);
</style>
<!--<![endif]-->
<style type="text/css">
@media only screen and (min-width: 480px) {
.mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
td.mj-full-width-mobile {
width: auto !important;
}
}
</style>
</head>
<body style="word-spacing: normal; background-color: #f4f4f4">
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td
style="direction:ltr;font-size:0px;padding:20px 0px 5px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align:top;" width="100%">
<tbody>
<tr>
<td align="center"
style="background:#ffffff;font-size:0px;padding:10px 25px 10px 25px;padding-right:25px;padding-left:25px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
<tbody>
<tr>
<td style="width:550px;">
<a href="https://manifold.markets" target="_blank">
<img alt="banner logo" height="auto"
src="https://manifold.markets/logo-banner.png"
style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
title="" width="550">
</a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="
background: #ffffff;
background-color: #ffffff;
margin: 0px auto;
max-width: 600px;
">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="background: #ffffff; background-color: #ffffff; width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 0px 0px;
padding-bottom: 0px;
padding-left: 0px;
padding-right: 0px;
padding-top: 20px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="vertical-align: top" width="100%">
<tbody>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
{{creatorName}}, (who you're following) just created a new market, check it out!</span></p>
</div>
</td>
</tr>
<tr>
<td align="center"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:center;color:#000000;">
<a href="{{questionUrl}}">
<img alt="{{questionTitle}}" width="375" height="200"
style="border: 1px solid #4337c9;" src="{{questionImgSrc}}">
</a>
</div>
<table cellspacing="0" cellpadding="0">
<tr>
<td style="border-radius: 4px;" bgcolor="#4337c9">
<a href="{{questionUrl}}" target="_blank"
style="padding: 6px 10px; border: 1px solid #4337c9;border-radius: 12px;font-family: Helvetica, Arial, sans-serif;font-size: 16px; color: #ffffff;text-decoration: none;font-weight:semibold;display: inline-block;">
View market
</a>
</td>
</tr>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 0 0 20px 0;
text-align: center;
">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin: 0px auto; max-width: 600px">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width: 100%">
<tbody>
<tr>
<td style="
direction: ltr;
font-size: 0px;
padding: 20px 0px 20px 0px;
text-align: center;
">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
width="100%">
<tbody>
<tr>
<td style="vertical-align: top; padding: 0">
<table border="0" cellpadding="0" cellspacing="0"
role="presentation" width="100%">
<tbody>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
">
<div style="
font-family: Ubuntu, Helvetica, Arial,
sans-serif;
font-size: 11px;
line-height: 22px;
text-align: center;
color: #000000;
">
<p style="margin: 10px 0">
This e-mail has been sent to
{{name}},
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</p>
</div>
</td>
</tr>
<tr>
<td align="center" style="
font-size: 0px;
padding: 10px 25px;
word-break: break-word;
"></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View File

@ -0,0 +1,397 @@
<!DOCTYPE html>
<html style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>New unique traders on your market</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
" bgcolor="#f6f6f6">
<table class="body-wrap" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
" bgcolor="#f6f6f6">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
" valign="top"></td>
<td class="container" width="600" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
" valign="top">
<div class="content" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
">
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
" bgcolor="#fff">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-wrap aligncenter" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
" align="center" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 0px 0;
text-align: left;
" valign="top">
<a href="https://manifold.markets" target="_blank">
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
alt="Manifold Markets" />
</a>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> just got its first trade from a user!
<br/>
<br/>
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for
creating a market that appeals to others, and we'll do so for each new trader.
<br/>
<br/>
Keep up the good work and check out your newest trader below!
</span></p>
</div>
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
">
<td class="content-block aligncenter" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
" align="center" valign="top">
<table class="invoice" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
margin-top: 10px;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor1Name}}</span>
{{bet1Description}}
</div>
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
">
<td style="padding: 20px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a href="{{marketUrl}}" target="_blank" style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
">
<span style="
display: block;
padding: 10px 20px;
line-height: 120%;
"><span style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
">View market</span></span>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div class="footer" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
">
<table width="100%" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="aligncenter content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
" align="center" valign="top">
Questions? Come ask in
<a href="https://discord.gg/eHQBNBqXuh" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
">our Discord</a>! Or,
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
" valign="top"></td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,501 @@
<!DOCTYPE html>
<html style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>New unique traders on your market</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h1 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h2 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h3 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h4 {
font-weight: 800 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
.invoice {
width: 100% !important;
}
}
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6em;
background-color: #f6f6f6;
margin: 0;
" bgcolor="#f6f6f6">
<table class="body-wrap" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
background-color: #f6f6f6;
margin: 0;
" bgcolor="#f6f6f6">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
" valign="top"></td>
<td class="container" width="600" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
display: block !important;
max-width: 600px !important;
clear: both !important;
margin: 0 auto;
" valign="top">
<div class="content" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
max-width: 600px;
display: block;
margin: 0 auto;
padding: 20px;
">
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
border-radius: 3px;
background-color: #fff;
margin: 0;
border: 1px solid #e9e9e9;
" bgcolor="#fff">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-wrap aligncenter" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 20px;
" align="center" valign="top">
<table width="100%" cellpadding="0" cellspacing="0" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
width: 90%;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
margin: 0;
padding: 0 0 0px 0;
text-align: left;
" valign="top">
<a href="https://manifold.markets" target="_blank">
<img src="https://manifold.markets/logo-banner.png" width="300" style="height: auto"
alt="Manifold Markets" />
</a>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
</span>Hi {{name}},</p>
</div>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:20px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content"
style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
data-testid="4XoHRGw1Y"><span
style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
Your market <a href='{{marketUrl}}'>{{marketTitle}}</a> has attracted {{totalPredictors}} total traders!
<br/>
<br/>
We sent you a <span style='color: #11b981'>{{bonusString}}</span> bonus for getting {{newPredictors}} new traders,
and we'll continue to do so for each new trader, (although we won't send you any more emails about it for this market).
<br/>
<br/>
Keep up the good work and check out your newest traders below!
</span></p>
</div>
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
">
<td class="content-block aligncenter" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
vertical-align: top;
text-align: center;
margin: 0;
padding: 0;
" align="center" valign="top">
<table class="invoice" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
text-align: left;
width: 80%;
margin: 40px auto;
margin-top: 10px;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor1AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor1Name}}</span>
{{bet1Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor2AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor2Name}}</span>
{{bet2Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor3AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor3Name}}</span>
{{bet3Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor4AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor4Name}}</span>
{{bet4Description}}
</div>
</td>
</tr><tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin-top: 10px;
">
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
padding: 10px 0;
" valign="top">
<div>
<img src="{{bettor5AvatarUrl}}" width="30" height="30" style="
border-radius: 30px;
overflow: hidden;
vertical-align: middle;
margin-right: 4px;
" alt="" />
<span style="font-weight: bold">{{bettor5Name}}</span>
{{bet5Description}}
</div>
</td>
</tr>
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 16px;
margin: 0;
">
<td style="padding: 20px 0 0 0; margin: 0">
<div align="center">
<!--[if mso]><table width="100%" cellpadding="0" cellspacing="0" border="0" style="border-spacing: 0; border-collapse: collapse; mso-table-lspace:0pt; mso-table-rspace:0pt;font-family:arial,helvetica,sans-serif;"><tr><td style="font-family:arial,helvetica,sans-serif;" align="center"><v:roundrect xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="urn:schemas-microsoft-com:office:word" href="" style="height:37px; v-text-anchor:middle; width:110px;" arcsize="11%" stroke="f" fillcolor="#3AAEE0"><w:anchorlock/><center style="color:#FFFFFF;font-family:arial,helvetica,sans-serif;"><![endif]-->
<a href="{{marketUrl}}" target="_blank" style="
box-sizing: border-box;
display: inline-block;
font-family: arial, helvetica, sans-serif;
text-decoration: none;
-webkit-text-size-adjust: none;
text-align: center;
color: #ffffff;
background-color: #11b981;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
width: auto;
max-width: 100%;
overflow-wrap: break-word;
word-break: break-word;
word-wrap: break-word;
mso-border-alt: none;
">
<span style="
display: block;
padding: 10px 20px;
line-height: 120%;
"><span style="
font-size: 16px;
font-weight: bold;
line-height: 18.8px;
">View market</span></span>
</a>
<!--[if mso]></center></v:roundrect></td></tr></table><![endif]-->
</div>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
<div class="footer" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
width: 100%;
clear: both;
color: #999;
margin: 0;
padding: 20px;
">
<table width="100%" style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<tr style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
margin: 0;
">
<td class="aligncenter content-block" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
vertical-align: top;
color: #999;
text-align: center;
margin: 0;
padding: 0 0 20px;
" align="center" valign="top">
Questions? Come ask in
<a href="https://discord.gg/eHQBNBqXuh" style="
font-family: 'Helvetica Neue', Helvetica, Arial,
sans-serif;
box-sizing: border-box;
font-size: 12px;
color: #999;
text-decoration: underline;
margin: 0;
">our Discord</a>! Or,
<a href="{{unsubscribeUrl}}" style="
color: inherit;
text-decoration: none;
" target="_blank">click here to unsubscribe from this type of notification</a>.
</td>
</tr>
</table>
</div>
</div>
</td>
<td style="
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
vertical-align: top;
margin: 0;
" valign="top"></td>
</tr>
</table>
</body>
</html>

View File

@ -1,519 +1,316 @@
<!DOCTYPE html> <!DOCTYPE html>
<html <html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:o="urn:schemas-microsoft-com:office:office" <head>
> <title>Manifold Markets 7th Day Anniversary Gift!</title>
<head> <!--[if !mso]><!-->
<title>7th Day Anniversary Gift!</title> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--[if !mso]><!--> <!--<![endif]-->
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<!--<![endif]--> <meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <style type="text/css">
<meta name="viewport" content="width=device-width,initial-scale=1" />
<style type="text/css">
#outlook a { #outlook a {
padding: 0; padding: 0;
} }
body { body {
margin: 0; margin: 0;
padding: 0; padding: 0;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%; -ms-text-size-adjust: 100%;
} }
table, table,
td { td {
border-collapse: collapse; border-collapse: collapse;
mso-table-lspace: 0pt; mso-table-lspace: 0pt;
mso-table-rspace: 0pt; mso-table-rspace: 0pt;
} }
img { img {
border: 0; border: 0;
height: auto; height: auto;
line-height: 100%; line-height: 100%;
outline: none; outline: none;
text-decoration: none; text-decoration: none;
-ms-interpolation-mode: bicubic; -ms-interpolation-mode: bicubic;
} }
p { p {
display: block; display: block;
margin: 13px 0; margin: 13px 0;
} }
</style> </style>
<!--[if mso]> <!--[if mso]>
<noscript> <noscript>
<xml> <xml>
<o:OfficeDocumentSettings> <o:OfficeDocumentSettings>
<o:AllowPNG /> <o:AllowPNG/>
<o:PixelsPerInch>96</o:PixelsPerInch> <o:PixelsPerInch>96</o:PixelsPerInch>
</o:OfficeDocumentSettings> </o:OfficeDocumentSettings>
</xml> </noscript </xml>
>z </noscript>
<![endif]--> <![endif]-->
<!--[if lte mso 11]> <!--[if lte mso 11]>
<style type="text/css"> <style type="text/css">
.mj-outlook-group-fix { .mj-outlook-group-fix { width:100% !important; }
width: 100% !important; </style>
} <![endif]-->
</style> <style type="text/css">
<![endif]--> @media only screen and (min-width:480px) {
<style type="text/css"> .mj-column-per-100 {
@media only screen and (min-width: 480px) { width: 100% !important;
.mj-column-per-100 { max-width: 100%;
}
}
</style>
<style media="screen and (min-width:480px)">
.moz-text-html .mj-column-per-100 {
width: 100% !important; width: 100% !important;
max-width: 100%; max-width: 100%;
}
} }
</style> </style>
<style media="screen and (min-width:480px)"> <style type="text/css">
.moz-text-html .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
[owa] .mj-column-per-100 { [owa] .mj-column-per-100 {
width: 100% !important;
max-width: 100%;
}
</style>
<style type="text/css">
@media only screen and (max-width: 480px) {
table.mj-full-width-mobile {
width: 100% !important; width: 100% !important;
} max-width: 100%;
td.mj-full-width-mobile {
width: auto !important;
}
} }
</style> </style>
</head> <style type="text/css">
@media only screen and (max-width:480px) {
table.mj-full-width-mobile {
width: 100% !important;
}
<body style="word-spacing: normal; background-color: #f4f4f4"> td.mj-full-width-mobile {
<div style="background-color: #f4f4f4"> width: auto !important;
<!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> }
<div }
style=" </style>
background: #ffffff; </head>
background-color: #ffffff;
margin: 0px auto; <body style="word-spacing:normal;background-color:#F4F4F4;">
max-width: 600px; <div style="background-color:#F4F4F4;">
" <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
> <div style="background:#ffffff;background-color:#ffffff;margin:0px auto;max-width:600px;">
<table <table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
align="center" style="background:#ffffff;background-color:#ffffff;width:100%;">
border="0" <tbody>
cellpadding="0" <tr>
cellspacing="0" <td
role="presentation" style="direction:ltr;font-size:0px;padding:0px 0px 0px 0px;padding-bottom:0px;padding-left:0px;padding-right:0px;padding-top:0px;text-align:center;">
style="background: #ffffff; background-color: #ffffff; width: 100%" <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
> <div class="mj-column-per-100 mj-outlook-group-fix"
<tbody> style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<tr> <table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
<td width="100%">
style=" <tbody>
direction: ltr; <tr>
font-size: 0px; <td align="center"
padding: 0px 0px 0px 0px; style="font-size:0px;padding:0px 25px 0px 25px;padding-top:0px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
padding-bottom: 0px; <table border="0" cellpadding="0" cellspacing="0" role="presentation"
padding-left: 0px; style="border-collapse:collapse;border-spacing:0px;">
padding-right: 0px;
padding-top: 0px;
text-align: center;
"
>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div
class="mj-column-per-100 mj-outlook-group-fix"
style="
font-size: 0px;
text-align: left;
direction: ltr;
display: inline-block;
vertical-align: top;
width: 100%;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="vertical-align: top"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td <td style="width:550px;"><a href="https://manifold.markets/home" target="_blank"><img
align="center" alt="" height="auto" src="https://i.imgur.com/8EP8Y8q.gif"
style=" style="border:none;display:block;outline:none;text-decoration:none;height:auto;width:100%;font-size:13px;"
font-size: 0px; width="550"></a></td>
padding: 0px 25px 0px 25px; </tr>
padding-top: 0px;
padding-right: 25px;
padding-bottom: 0px;
padding-left: 25px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody>
<tr>
<td style="width: 550px">
<a
href="https://manifold.markets/home"
target="_blank"
><img
alt=""
height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/u71/sjvu.gif"
style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="550"
/></a>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="left"
style="
font-size: 0px;
padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word;
"
>
<div
style="
font-family: Arial, sans-serif;
font-size: 18px;
letter-spacing: normal;
line-height: 1;
text-align: left;
color: #000000;
"
>
<p
class="text-build-content"
style="
text-align: center;
margin: 10px 0;
margin-top: 10px;
margin-bottom: 10px;
"
data-testid="4XoHRGw1Y"
>
<span
style="
color: #000000;
font-family: Arial, Helvetica, sans-serif;
font-size: 18px;
"
>Hopefully you haven&#39;t gambled all your M$
away already... but if you have I bring good
news! Click the link below to recieve a one time
gift of M$ 500 to your account!</span
>
</p>
</div>
</td>
</tr>
<tr>
<td
align="center"
style="
font-size: 0px;
padding: 10px 25px 25px 25px;
padding-top: 10px;
padding-right: 25px;
padding-bottom: 25px;
padding-left: 25px;
word-break: break-word;
"
>
<table
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
style="
border-collapse: collapse;
border-spacing: 0px;
"
>
<tbody>
<tr>
<td style="width: 550px">
<a href="{{manalink}}" target="_blank">
<img
alt="Get M$500"
height="auto"
src="https://03jlj.mjt.lu/img/03jlj/b/u71/sjgt.png"
style="
border: none;
display: block;
outline: none;
text-decoration: none;
height: auto;
width: 100%;
font-size: 13px;
"
width="550"
/></a>
<< /td>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td
align="left"
style="
font-size: 0px;
padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word;
"
>
<div
style="
font-family: Arial, sans-serif;
font-size: 18px;
letter-spacing: normal;
line-height: 1;
text-align: left;
color: #000000;
"
>
<p
class="text-build-content"
style="
line-height: 23px;
text-align: center;
margin: 10px 0;
margin-top: 10px;
"
data-testid="3Q8BP69fq"
>
<span
style="
color: #000000;
font-family: Arial, Helvetica, sans-serif;
font-size: 18px;
"
>If you are still engaging with our markets then
at this point you might as well join our </span
><a
class="link-build-content"
style="color: inherit; text-decoration: none"
target="_blank"
href="https://discord.gg/VARzUpyCSa"
><span
style="
color: #0c21bf;
font-family: Arial;
font-size: 18px;
"
><u>Discord server</u></span
><span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
><u>.</u>
</span></a
><span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>You can always leave if you dont like it but
I&#39;d be willing to make a market betting
you&#39;ll stay.</span
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
></p>
<br />
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
>
<span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>Cheers,</span
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0"
>
<span
style="
color: #000000;
font-family: Arial;
font-size: 18px;
"
>David from Manifold</span
>
</p>
<p
class="text-build-content"
data-testid="3Q8BP69fq"
style="margin: 10px 0; margin-bottom: 10px"
></p>
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </td>
<!--[if mso | IE]></td></tr></table><![endif]--> </tr>
</td> <tr>
</tr> <td align="left"
</tbody> style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
</table> <div
</div> style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]--> <p class="text-build-content"
<div style="margin: 0px auto; max-width: 600px"> style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
<table data-testid="4XoHRGw1Y"><span
align="center" style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">
border="0" Hi {{name}},</span></p>
cellpadding="0" </div>
cellspacing="0" </td>
role="presentation" </tr>
style="width: 100%" <tr>
> <td align="left"
<tbody> style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<tr> <div
<td style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
style=" <p class="text-build-content"
direction: ltr; style="line-height: 24px; margin: 10px 0; margin-top: 10px; margin-bottom: 10px;"
font-size: 0px; data-testid="4XoHRGw1Y"><span
padding: 20px 0px 20px 0px; style="color:#000000;font-family:Arial, Helvetica, sans-serif;font-size:18px;">Thanks for
text-align: center; using Manifold Markets. Running low
" on mana (M$)? Click the link below to receive a one time gift of M$500!</span></p>
> </div>
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]--> </td>
<div </tr>
class="mj-column-per-100 mj-outlook-group-fix" <tr>
style=" <td>
font-size: 0px; <p></p>
text-align: left; </td>
direction: ltr; </tr>
display: inline-block; <tr>
vertical-align: top; <td align="center">
width: 100%; <table cellspacing="0" cellpadding="0">
" <tr>
> <td>
<table <table cellspacing="0" cellpadding="0">
border="0" <tr>
cellpadding="0" <td style="border-radius: 2px;" bgcolor="#4337c9">
cellspacing="0" <a href="{{manalink}}" target="_blank"
role="presentation" style="padding: 12px 16px; border: 1px solid #4337c9;border-radius: 16px;font-family: Helvetica, Arial, sans-serif;font-size: 24px; color: #ffffff;text-decoration: none;font-weight:bold;display: inline-block;">
width="100%" Claim M$500
> </a>
<tbody> </td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td align="left"
style="font-size:0px;padding:15px 25px 0px 25px;padding-top:15px;padding-right:25px;padding-bottom:0px;padding-left:25px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:18px;letter-spacing:normal;line-height:1;text-align:left;color:#000000;">
<p class="text-build-content" style="line-height: 23px; margin: 10px 0; margin-top: 10px;"
data-testid="3Q8BP69fq"><span style="font-family:Arial, sans-serif;font-size:18px;">Did
you know, besides making correct predictions, there are
plenty of other ways to earn mana?</span></p>
<ul>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Predicting
consecutive days to earn streak rewards</span></li>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Receiving
tips on comments and markets</span></li>
<li style="line-height:23px;"><span
style="font-family:Arial, sans-serif;font-size:18px;">Unique
trader bonus for each user who trades on your
markets</span></li>
<li style="line-height:23px;"><span style="font-family:Arial, sans-serif;font-size:18px;"><a
class="link-build-content" style="color:inherit;; text-decoration: none;"
target="_blank" href="https://manifold.markets/referrals"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Referring
friends</u></span></a></span></li>
<li style="line-height:23px;"><a class="link-build-content"
style="color:inherit;; text-decoration: none;" target="_blank"
href="https://manifold.markets/group/bugs?s=most-traded"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>Reporting
bugs</u></span></a><span style="font-family:Arial, sans-serif;font-size:18px;">
and </span><a class="link-build-content" style="color:inherit;; text-decoration: none;"
target="_blank"
href="https://manifold.markets/group/manifold-features-25bad7c7792e/chat?s=most-traded"><span
style="color:#55575d;font-family:Arial;font-size:18px;"><u>giving
feedback</u></span></a></li>
</ul>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;">&nbsp;</p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
style="color:#000000;font-family:Arial;font-size:18px;">Cheers,</span>
</p>
<p class="text-build-content" data-testid="3Q8BP69fq" style="margin: 10px 0;"><span
style="color:#000000;font-family:Arial;font-size:18px;">David
from Manifold</span></p>
<p class="text-build-content" data-testid="3Q8BP69fq"
style="margin: 10px 0; margin-bottom: 10px;">&nbsp;</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><![endif]-->
</td>
</tr>
</tbody>
</table>
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation" style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:0 0 20px 0;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;"
width="100%">
<tbody>
<tr>
<td align="center" style="font-size:0px;padding:0px;word-break:break-word;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation"
style="border-collapse:collapse;border-spacing:0px;">
</div>
<!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
<div style="margin:0px auto;max-width:600px;">
<table align="center" border="0" cellpadding="0" cellspacing="0" role="presentation"
style="width:100%;">
<tbody>
<tr>
<td style="direction:ltr;font-size:0px;padding:20px 0px 20px 0px;text-align:center;">
<!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix"
style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
<tbody>
<tr> <tr>
<td style="vertical-align: top; padding: 0"> <td style="vertical-align:top;padding:0;">
<table <table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
border="0"
cellpadding="0"
cellspacing="0"
role="presentation"
width="100%"
>
<tbody> <tbody>
<tr> <tr>
<td <td align="center" style="
align="center"
style="
font-size: 0px; font-size: 0px;
padding: 10px 25px; padding: 10px 25px;
padding-top: 0px;
padding-bottom: 0px;
word-break: break-word; word-break: break-word;
" ">
> <div style="
<div font-family: Ubuntu, Helvetica, Arial,
style=" sans-serif;
font-family: Arial, sans-serif;
font-size: 11px; font-size: 11px;
letter-spacing: normal;
line-height: 22px; line-height: 22px;
text-align: center; text-align: center;
color: #000000; color: #000000;
" ">
> <p style="margin: 10px 0">
<p style="margin: 10px 0"> This e-mail has been sent to {{name}},
This e-mail has been sent to {{name}}, <a href="{{unsubscribeUrl}}" style="
<a
href="{{unsubscribeLink}}"
style="
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
" " target="_blank">click here to unsubscribe from this type of notification</a>.
target="_blank" </p>
>click here to unsubscribe</a </div>
>. </td>
</p> </tr>
</div> <tr>
</td> <td align="center"
</tr> style="font-size:0px;padding:10px 25px;padding-top:0px;padding-bottom:0px;word-break:break-word;">
<div
style="font-family:Arial, sans-serif;font-size:11px;letter-spacing:normal;line-height:22px;text-align:center;color:#000000;">
</div>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<!--[if mso | IE]></td></tr></table><![endif]--> <!--[if mso | IE]></td></tr></table><![endif]-->
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<!--[if mso | IE]></td></tr></table><![endif]--> <!--[if mso | IE]></td></tr></table><![endif]-->
</div> </div>
</body> </body>
</html>
</html>

Some files were not shown because too many files have changed in this diff Show More