From c42332627036446869412917d7f22899b5e96ec0 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Tue, 13 Sep 2022 16:12:53 -0600 Subject: [PATCH 1/6] Send users emails when they hit 1 and 6 unique bettors --- functions/src/create-notification.ts | 101 ++-- .../email-templates/new-unique-bettor.html | 397 ++++++++++++++ .../email-templates/new-unique-bettors.html | 501 ++++++++++++++++++ functions/src/emails.ts | 61 +++ functions/src/on-create-bet.ts | 6 +- web/components/notification-settings.tsx | 7 +- 6 files changed, 1039 insertions(+), 34 deletions(-) create mode 100644 functions/src/email-templates/new-unique-bettor.html create mode 100644 functions/src/email-templates/new-unique-bettors.html diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 84edf715..e2959dda 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -8,7 +8,7 @@ import { User } from '../../common/user' import { Contract } from '../../common/contract' import { getPrivateUser, getValues } from './utils' import { Comment } from '../../common/comment' -import { uniq } from 'lodash' +import { groupBy, uniq } from 'lodash' import { Bet, LimitBet } from '../../common/bet' import { Answer } from '../../common/answer' import { getContractBetMetrics } from '../../common/calculate' @@ -23,6 +23,7 @@ import { sendNewAnswerEmail, sendNewCommentEmail, sendNewFollowedMarketEmail, + sendNewUniqueBettorsEmail, } from './emails' import { filterDefined } from '../../common/util/array' const firestore = admin.firestore() @@ -774,44 +775,84 @@ export const createUniqueBettorBonusNotification = async ( txnId: string, contract: Contract, amount: number, + uniqueBettorIds: string[], idempotencyKey: string ) => { - console.log('createUniqueBettorBonusNotification') const privateUser = await getPrivateUser(contractCreatorId) if (!privateUser) return - const { sendToBrowser } = await getDestinationsForUser( + const { sendToBrowser, sendToEmail } = await getDestinationsForUser( privateUser, 'unique_bettors_on_your_contract' ) - if (!sendToBrowser) return - - const notificationRef = firestore - .collection(`/users/${contractCreatorId}/notifications`) - .doc(idempotencyKey) - const notification: Notification = { - id: idempotencyKey, - userId: contractCreatorId, - reason: 'unique_bettors_on_your_contract', - createdTime: Date.now(), - isSeen: false, - sourceId: txnId, - sourceType: 'bonus', - sourceUpdateType: 'created', - sourceUserName: bettor.name, - sourceUserUsername: bettor.username, - sourceUserAvatarUrl: bettor.avatarUrl, - sourceText: amount.toString(), - sourceSlug: contract.slug, - sourceTitle: contract.question, - // Perhaps not necessary, but just in case - sourceContractSlug: contract.slug, - sourceContractId: contract.id, - sourceContractTitle: contract.question, - sourceContractCreatorUsername: contract.creatorUsername, + if (sendToBrowser) { + const notificationRef = firestore + .collection(`/users/${contractCreatorId}/notifications`) + .doc(idempotencyKey) + const notification: Notification = { + id: idempotencyKey, + userId: contractCreatorId, + reason: 'unique_bettors_on_your_contract', + createdTime: Date.now(), + isSeen: false, + sourceId: txnId, + sourceType: 'bonus', + sourceUpdateType: 'created', + sourceUserName: bettor.name, + sourceUserUsername: bettor.username, + sourceUserAvatarUrl: bettor.avatarUrl, + sourceText: amount.toString(), + sourceSlug: contract.slug, + sourceTitle: contract.question, + // Perhaps not necessary, but just in case + sourceContractSlug: contract.slug, + sourceContractId: contract.id, + sourceContractTitle: contract.question, + sourceContractCreatorUsername: contract.creatorUsername, + } + await notificationRef.set(removeUndefinedProps(notification)) } - return await notificationRef.set(removeUndefinedProps(notification)) - // TODO send email notification + if (!sendToEmail) return + const uniqueBettorsExcludingCreator = uniqueBettorIds.filter( + (id) => id !== contractCreatorId + ) + // only send on 1st and 6th bettor + if ( + uniqueBettorsExcludingCreator.length !== 1 && + uniqueBettorsExcludingCreator.length !== 6 + ) + return + const totalNewBettorsToReport = + uniqueBettorsExcludingCreator.length === 1 ? 1 : 5 + + const mostRecentUniqueBettors = await getValues( + firestore + .collection('users') + .where( + 'id', + 'in', + uniqueBettorsExcludingCreator.slice( + uniqueBettorsExcludingCreator.length - totalNewBettorsToReport, + uniqueBettorsExcludingCreator.length + ) + ) + ) + + const bets = await getValues( + firestore.collection('contracts').doc(contract.id).collection('bets') + ) + // group bets by bettors + const bettorsToTheirBets = groupBy(bets, (bet) => bet.userId) + await sendNewUniqueBettorsEmail( + 'unique_bettors_on_your_contract', + contractCreatorId, + privateUser, + contract, + uniqueBettorsExcludingCreator.length, + mostRecentUniqueBettors, + bettorsToTheirBets, + Math.round(amount * totalNewBettorsToReport) + ) } export const createNewContractNotification = async ( diff --git a/functions/src/email-templates/new-unique-bettor.html b/functions/src/email-templates/new-unique-bettor.html new file mode 100644 index 00000000..30da8b99 --- /dev/null +++ b/functions/src/email-templates/new-unique-bettor.html @@ -0,0 +1,397 @@ + + + + + + + New unique predictors on your market + + + + + + + + + + + +
+
+ + + + +
+ + + + + + + + + + + + + +
+ + Manifold Markets + +
+
+

+ Hi {{name}},

+
+
+
+

+ Your market {{marketTitle}} just got its first prediction from a user! +
+
+ We sent you a {{bonusString}} bonus for + creating a market that appeals to others, and we'll do so for each new predictor. +
+
+ Keep up the good work and check out your newest predictor below! +

+
+
+ + + + + + + +
+
+ + {{bettor1Name}} + {{bet1Description}} +
+
+ +
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/functions/src/email-templates/new-unique-bettors.html b/functions/src/email-templates/new-unique-bettors.html new file mode 100644 index 00000000..eb4c04e2 --- /dev/null +++ b/functions/src/email-templates/new-unique-bettors.html @@ -0,0 +1,501 @@ + + + + + + + New unique predictors on your market + + + + + + + + + + + +
+
+ + + + +
+ + + + + + + + + + + + + +
+ + Manifold Markets + +
+
+

+ Hi {{name}},

+
+
+
+

+ Your market {{marketTitle}} got predictions from a total of {{totalPredictors}} users! +
+
+ We sent you a {{bonusString}} bonus for getting {{newPredictors}} new predictors, + and we'll continue to do so for each new predictor, (although we won't send you any more emails about it for this market). +
+
+ Keep up the good work and check out your newest predictors below! +

+
+
+ + + + + + + + + + + + + + + +
+
+ + {{bettor1Name}} + {{bet1Description}} +
+
+
+ + {{bettor2Name}} + {{bet2Description}} +
+
+
+ + {{bettor3Name}} + {{bet3Description}} +
+
+
+ + {{bettor4Name}} + {{bet4Description}} +
+
+
+ + {{bettor5Name}} + {{bet5Description}} +
+
+ +
+
+
+ +
+
+ + + \ No newline at end of file diff --git a/functions/src/emails.ts b/functions/src/emails.ts index da6a5b41..adeb3d12 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -22,6 +22,7 @@ import { notification_reason_types, getDestinationsForUser, } from '../../common/notification' +import { Dictionary } from 'lodash' export const sendMarketResolutionEmail = async ( reason: notification_reason_types, @@ -544,3 +545,63 @@ export const sendNewFollowedMarketEmail = async ( } ) } +export const sendNewUniqueBettorsEmail = async ( + reason: notification_reason_types, + userId: string, + privateUser: PrivateUser, + contract: Contract, + totalPredictors: number, + newPredictors: User[], + userBets: Dictionary<[Bet, ...Bet[]]>, + bonusAmount: number +) => { + const { sendToEmail, urlToManageThisNotification: unsubscribeUrl } = + await getDestinationsForUser(privateUser, reason) + if (!privateUser.email || !sendToEmail) return + const user = await getUser(privateUser.id) + if (!user) return + + const { name } = user + const firstName = name.split(' ')[0] + const creatorName = contract.creatorName + // make the emails stack for the same contract + const subject = `You made a popular market! ${ + contract.question.length > 50 + ? contract.question.slice(0, 50) + '...' + : contract.question + } just got ${ + newPredictors.length + } new predictions. Check out who's betting on it inside.` + const templateData: Record = { + name: firstName, + creatorName, + totalPredictors: totalPredictors.toString(), + bonusString: formatMoney(bonusAmount), + marketTitle: contract.question, + marketUrl: contractUrl(contract), + unsubscribeUrl, + newPredictors: newPredictors.length.toString(), + } + + newPredictors.forEach((p, i) => { + templateData[`bettor${i + 1}Name`] = p.name + if (p.avatarUrl) templateData[`bettor${i + 1}AvatarUrl`] = p.avatarUrl + const bet = userBets[p.id][0] + if (bet) { + const { amount, sale } = bet + templateData[`bet${i + 1}Description`] = `${ + sale || amount < 0 ? 'sold' : 'bought' + } ${formatMoney(Math.abs(amount))}` + } + }) + + return await sendTemplateEmail( + privateUser.email, + subject, + newPredictors.length === 1 ? 'new-unique-bettor' : 'new-unique-bettors', + templateData, + { + from: `Manifold Markets `, + } + ) +} diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index 5dbebfc3..f2c6b51a 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -28,8 +28,9 @@ import { User } from '../../common/user' const firestore = admin.firestore() const BONUS_START_DATE = new Date('2022-07-13T15:30:00.000Z').getTime() -export const onCreateBet = functions.firestore - .document('contracts/{contractId}/bets/{betId}') +export const onCreateBet = functions + .runWith({ secrets: ['MAILGUN_KEY'] }) + .firestore.document('contracts/{contractId}/bets/{betId}') .onCreate(async (change, context) => { const { contractId } = context.params as { contractId: string @@ -198,6 +199,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( result.txn.id, contract, result.txn.amount, + newUniqueBettorIds, eventId + '-unique-bettor-bonus' ) } diff --git a/web/components/notification-settings.tsx b/web/components/notification-settings.tsx index 65804991..128a89ef 100644 --- a/web/components/notification-settings.tsx +++ b/web/components/notification-settings.tsx @@ -60,11 +60,14 @@ export function NotificationSettings(props: { 'tagged_user', // missing tagged on contract description email 'contract_from_followed_user', + 'unique_bettors_on_your_contract', // TODO: add these + // one-click unsubscribe only unsubscribes them from that type only, (well highlighted), then a link to manage the rest of their notifications + // 'profit_loss_updates', - changes in markets you have shares in + // biggest winner, here are the rest of your markets + // 'referral_bonuses', - // 'unique_bettors_on_your_contract', // 'on_new_follow', - // 'profit_loss_updates', // 'tips_on_your_markets', // 'tips_on_your_comments', // maybe the following? From 34bad35cb88cf186df403d031c106c2f9f258e2a Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Tue, 13 Sep 2022 16:19:52 -0600 Subject: [PATCH 2/6] Betting=>predicting --- functions/src/emails.ts | 2 +- web/components/contract/contract-details.tsx | 2 +- web/components/notification-settings.tsx | 2 +- web/components/profile/betting-streak-modal.tsx | 7 ++++--- web/pages/create.tsx | 4 ++-- web/pages/notifications.tsx | 10 ++++++---- web/pages/tournaments/index.tsx | 2 +- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/functions/src/emails.ts b/functions/src/emails.ts index adeb3d12..e9ef9630 100644 --- a/functions/src/emails.ts +++ b/functions/src/emails.ts @@ -571,7 +571,7 @@ export const sendNewUniqueBettorsEmail = async ( : contract.question } just got ${ newPredictors.length - } new predictions. Check out who's betting on it inside.` + } new predictions. Check out who's predicting on it inside.` const templateData: Record = { name: firstName, creatorName, diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index c383d349..e28ab41a 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -294,7 +294,7 @@ export function ExtraMobileContractDetails(props: { {volumeTranslation} diff --git a/web/components/notification-settings.tsx b/web/components/notification-settings.tsx index 128a89ef..c45510ac 100644 --- a/web/components/notification-settings.tsx +++ b/web/components/notification-settings.tsx @@ -134,7 +134,7 @@ export function NotificationSettings(props: { const bonuses: sectionData = { label: 'Bonuses', subscriptionTypeToDescription: { - betting_streaks: 'Betting streak bonuses', + betting_streaks: 'Prediction streak bonuses', referral_bonuses: 'Referral bonuses from referring users', unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets', }, diff --git a/web/components/profile/betting-streak-modal.tsx b/web/components/profile/betting-streak-modal.tsx index 694a0193..a137833c 100644 --- a/web/components/profile/betting-streak-modal.tsx +++ b/web/components/profile/betting-streak-modal.tsx @@ -16,13 +16,14 @@ export function BettingStreakModal(props: { 🔥 - Daily betting streaks + Daily prediction streaks • What are they? You get {formatMoney(BETTING_STREAK_BONUS_AMOUNT)} more for each day - of consecutive betting up to {formatMoney(BETTING_STREAK_BONUS_MAX)} - . The more days you bet in a row, the more you earn! + of consecutive predicting up to{' '} + {formatMoney(BETTING_STREAK_BONUS_MAX)}. The more days you predict + in a row, the more you earn! • Where can I check my streak? diff --git a/web/pages/create.tsx b/web/pages/create.tsx index 5fb9549e..f5d1c605 100644 --- a/web/pages/create.tsx +++ b/web/pages/create.tsx @@ -426,7 +426,7 @@ export function NewContract(props: {
Cost {!deservesFreeMarket ? ( diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 7ebc473b..1e8fbb4d 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -428,7 +428,7 @@ function IncomeNotificationItem(props: { reasonText = !simple ? `Bonus for ${ parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT - } new traders on` + } new predictors on` : 'bonus on' } else if (sourceType === 'tip') { reasonText = !simple ? `tipped you on` : `in tips on` @@ -436,7 +436,7 @@ function IncomeNotificationItem(props: { if (sourceText && +sourceText === 50) reasonText = '(max) for your' else reasonText = 'for your' } else if (sourceType === 'loan' && sourceText) { - reasonText = `of your invested bets returned as a` + reasonText = `of your invested predictions returned as a` // TODO: support just 'like' notification without a tip } else if (sourceType === 'tip_and_like' && sourceText) { reasonText = !simple ? `liked` : `in likes on` @@ -448,7 +448,9 @@ function IncomeNotificationItem(props: { : user?.currentBettingStreak ?? 0 const bettingStreakText = sourceType === 'betting_streak_bonus' && - (sourceText ? `🔥 ${streakInDays} day Betting Streak` : 'Betting Streak') + (sourceText + ? `🔥 ${streakInDays} day Prediction Streak` + : 'Prediction Streak') return ( <> @@ -546,7 +548,7 @@ function IncomeNotificationItem(props: { {(isTip || isUniqueBettorBonus) && ( )} diff --git a/web/pages/tournaments/index.tsx b/web/pages/tournaments/index.tsx index 27c51c15..e81c239f 100644 --- a/web/pages/tournaments/index.tsx +++ b/web/pages/tournaments/index.tsx @@ -155,7 +155,7 @@ export default function TournamentPage(props: { sections: SectionInfo[] }) { {sections.map( From c9d323c83ff4249d11d556b71e199fae32ce5376 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 13 Sep 2022 17:47:29 -0500 Subject: [PATCH 3/6] 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 --- web/components/arrange-home.tsx | 2 +- web/components/bets-list.tsx | 5 +- web/components/visibility-observer.tsx | 12 ++-- web/hooks/use-contracts.ts | 37 +--------- web/pages/experimental/home/edit.tsx | 8 ++- web/pages/experimental/home/index.tsx | 99 ++++++++++++-------------- 6 files changed, 64 insertions(+), 99 deletions(-) diff --git a/web/components/arrange-home.tsx b/web/components/arrange-home.tsx index 646d30fe..6be187f8 100644 --- a/web/components/arrange-home.tsx +++ b/web/components/arrange-home.tsx @@ -111,9 +111,9 @@ export const getHomeItems = (groups: Group[], sections: string[]) => { if (!isArray(sections)) sections = [] const items = [ - { label: 'Daily movers', id: 'daily-movers' }, { label: 'Trending', id: 'score' }, { label: 'New for you', id: 'newest' }, + { label: 'Daily movers', id: 'daily-movers' }, ...groups.map((g) => ({ label: g.name, id: g.id, diff --git a/web/components/bets-list.tsx b/web/components/bets-list.tsx index ab232927..9c76174b 100644 --- a/web/components/bets-list.tsx +++ b/web/components/bets-list.tsx @@ -754,7 +754,10 @@ function SellButton(props: { ) } -function ProfitBadge(props: { profitPercent: number; className?: string }) { +export function ProfitBadge(props: { + profitPercent: number + className?: string +}) { const { profitPercent, className } = props if (!profitPercent) return null const colors = diff --git a/web/components/visibility-observer.tsx b/web/components/visibility-observer.tsx index aea2e41d..288d8f0e 100644 --- a/web/components/visibility-observer.tsx +++ b/web/components/visibility-observer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { useEvent } from '../hooks/use-event' export function VisibilityObserver(props: { @@ -8,18 +8,16 @@ export function VisibilityObserver(props: { const { className } = props const [elem, setElem] = useState(null) const onVisibilityUpdated = useEvent(props.onVisibilityUpdated) - const observer = useRef( - new IntersectionObserver(([entry]) => { - onVisibilityUpdated(entry.isIntersecting) - }, {}) - ).current useEffect(() => { if (elem) { + const observer = new IntersectionObserver(([entry]) => { + onVisibilityUpdated(entry.isIntersecting) + }, {}) observer.observe(elem) return () => observer.unobserve(elem) } - }, [elem, observer]) + }, [elem, onVisibilityUpdated]) return
} diff --git a/web/hooks/use-contracts.ts b/web/hooks/use-contracts.ts index 2f3bea7b..1ea2f232 100644 --- a/web/hooks/use-contracts.ts +++ b/web/hooks/use-contracts.ts @@ -1,10 +1,8 @@ import { useFirestoreQueryData } from '@react-query-firebase/firestore' -import { isEqual } from 'lodash' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' import { Contract, listenForActiveContracts, - listenForContract, listenForContracts, listenForHotContracts, listenForInactiveContracts, @@ -62,39 +60,6 @@ export const useHotContracts = () => { return hotContracts } -export const useUpdatedContracts = (contracts: Contract[] | undefined) => { - const [__, triggerUpdate] = useState(0) - const contractDict = useRef<{ [id: string]: Contract }>({}) - - useEffect(() => { - if (contracts === undefined) return - - contractDict.current = Object.fromEntries(contracts.map((c) => [c.id, c])) - - const disposes = contracts.map((contract) => { - const { id } = contract - - return listenForContract(id, (contract) => { - const curr = contractDict.current[id] - if (!isEqual(curr, contract)) { - contractDict.current[id] = contract as Contract - triggerUpdate((n) => n + 1) - } - }) - }) - - triggerUpdate((n) => n + 1) - - return () => { - disposes.forEach((dispose) => dispose()) - } - }, [!!contracts]) - - return contracts && Object.keys(contractDict.current).length > 0 - ? contracts.map((c) => contractDict.current[c.id]) - : undefined -} - export const usePrefetchUserBetContracts = (userId: string) => { const queryClient = useQueryClient() return queryClient.prefetchQuery( diff --git a/web/pages/experimental/home/edit.tsx b/web/pages/experimental/home/edit.tsx index 2ed9d2dd..8c242a34 100644 --- a/web/pages/experimental/home/edit.tsx +++ b/web/pages/experimental/home/edit.tsx @@ -28,7 +28,7 @@ export default function Home() { - + <Title text="Customize your home page" /> <DoneButton /> </Row> @@ -47,7 +47,11 @@ function DoneButton(props: { className?: string }) { return ( <SiteLink href="/experimental/home"> - <Button size="lg" color="blue" className={clsx(className, 'flex')}> + <Button + size="lg" + color="blue" + className={clsx(className, 'flex whitespace-nowrap')} + > Done </Button> </SiteLink> diff --git a/web/pages/experimental/home/index.tsx b/web/pages/experimental/home/index.tsx index 08f502b6..f5734918 100644 --- a/web/pages/experimental/home/index.tsx +++ b/web/pages/experimental/home/index.tsx @@ -1,7 +1,7 @@ import React from 'react' import Router from 'next/router' import { - PencilIcon, + AdjustmentsIcon, PlusSmIcon, ArrowSmRightIcon, } from '@heroicons/react/solid' @@ -26,11 +26,12 @@ import { Row } from 'web/components/layout/row' import { ProbChangeTable } from 'web/components/contract/prob-change-table' import { groupPath } from 'web/lib/firebase/groups' import { usePortfolioHistory } from 'web/hooks/use-portfolio-history' -import { calculatePortfolioProfit } from 'common/calculate-metrics' import { formatMoney } from 'common/util/format' import { useProbChanges } from 'web/hooks/use-prob-changes' +import { ProfitBadge } from 'web/components/bets-list' +import { calculatePortfolioProfit } from 'common/calculate-metrics' -const Home = () => { +export default function Home() { const user = useUser() useTracking('view home') @@ -44,14 +45,14 @@ const Home = () => { return ( <Page> <Col className="pm:mx-10 gap-4 px-4 pb-12"> - <Row className={'w-full items-center justify-between'}> - <Title className="!mb-0" text="Home" /> - - <EditButton /> + <Row className={'mt-4 w-full items-start justify-between'}> + <Row className="items-end gap-4"> + <Title className="!mb-1 !mt-0" text="Home" /> + <EditButton /> + </Row> + <DailyProfitAndBalance className="" user={user} /> </Row> - <DailyProfitAndBalance userId={user?.id} /> - {sections.map((item) => { const { id } = item if (id === 'daily-movers') { @@ -97,17 +98,10 @@ function SearchSection(props: { followed?: boolean }) { const { label, user, sort, yourBets, followed } = props - const href = `/home?s=${sort}` return ( <Col> - <SiteLink className="mb-2 text-xl" href={href}> - {label}{' '} - <ArrowSmRightIcon - className="mb-0.5 inline h-6 w-6 text-gray-500" - aria-hidden="true" - /> - </SiteLink> + <SectionHeader label={label} href={`/home?s=${sort}`} /> <ContractSearch user={user} defaultSort={sort} @@ -134,13 +128,7 @@ function GroupSection(props: { return ( <Col> - <SiteLink className="mb-2 text-xl" href={groupPath(group.slug)}> - {group.name}{' '} - <ArrowSmRightIcon - className="mb-0.5 inline h-6 w-6 text-gray-500" - aria-hidden="true" - /> - </SiteLink> + <SectionHeader label={group.name} href={groupPath(group.slug)} /> <ContractSearch user={user} defaultSort={'score'} @@ -159,15 +147,25 @@ function DailyMoversSection(props: { userId: string | null | undefined }) { return ( <Col className="gap-2"> - <SiteLink className="text-xl" href={'/daily-movers'}> - Daily movers{' '} + <SectionHeader label="Daily movers" href="daily-movers" /> + <ProbChangeTable changes={changes} /> + </Col> + ) +} + +function SectionHeader(props: { label: string; href: string }) { + const { label, href } = props + + return ( + <Row className="mb-3 items-center justify-between"> + <SiteLink className="text-xl" href={href}> + {label}{' '} <ArrowSmRightIcon className="mb-0.5 inline h-6 w-6 text-gray-500" aria-hidden="true" /> </SiteLink> - <ProbChangeTable changes={changes} /> - </Col> + </Row> ) } @@ -176,45 +174,42 @@ function EditButton(props: { className?: string }) { return ( <SiteLink href="/experimental/home/edit"> - <Button size="lg" color="gray-white" className={clsx(className, 'flex')}> - <PencilIcon className={clsx('mr-2 h-[24px] w-5')} aria-hidden="true" />{' '} - Edit + <Button size="sm" color="gray-white" className={clsx(className, 'flex')}> + <AdjustmentsIcon className={clsx('h-[24px] w-5')} aria-hidden="true" /> </Button> </SiteLink> ) } function DailyProfitAndBalance(props: { - userId: string | null | undefined + user: User | null | undefined className?: string }) { - const { userId, className } = props - const metrics = usePortfolioHistory(userId ?? '', 'daily') ?? [] + const { user, className } = props + const metrics = usePortfolioHistory(user?.id ?? '', 'daily') ?? [] const [first, last] = [metrics[0], metrics[metrics.length - 1]] if (first === undefined || last === undefined) return null const profit = calculatePortfolioProfit(last) - calculatePortfolioProfit(first) - - const balanceChange = last.balance - first.balance + const profitPercent = profit / first.investmentValue return ( - <div className={clsx(className, 'text-lg')}> - <span className={clsx(profit >= 0 ? 'text-green-500' : 'text-red-500')}> - {profit >= 0 && '+'} - {formatMoney(profit)} - </span>{' '} - profit and{' '} - <span - className={clsx(balanceChange >= 0 ? 'text-green-500' : 'text-red-500')} - > - {balanceChange >= 0 && '+'} - {formatMoney(balanceChange)} - </span>{' '} - balance today - </div> + <Row className={'gap-4'}> + <Col> + <div className="text-gray-500">Daily profit</div> + <Row className={clsx(className, 'items-center text-lg')}> + <span>{formatMoney(profit)}</span>{' '} + <ProfitBadge profitPercent={profitPercent * 100} /> + </Row> + </Col> + <Col> + <div className="text-gray-500">Streak</div> + <Row className={clsx(className, 'items-center text-lg')}> + <span>🔥 {user?.currentBettingStreak ?? 0}</span> + </Row> + </Col> + </Row> ) } - -export default Home From df3d7b591dfa234b1102ddf0cde936a7c4d35186 Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 13 Sep 2022 17:00:34 -0600 Subject: [PATCH 4/6] Componentize notification line setting, don't use useEffect --- web/components/notification-settings.tsx | 143 +++++++++++++---------- web/pages/notifications.tsx | 1 + 2 files changed, 85 insertions(+), 59 deletions(-) diff --git a/web/components/notification-settings.tsx b/web/components/notification-settings.tsx index c45510ac..61e3b9d9 100644 --- a/web/components/notification-settings.tsx +++ b/web/components/notification-settings.tsx @@ -1,11 +1,10 @@ -import { usePrivateUser } from 'web/hooks/use-user' -import React, { ReactNode, useEffect, useState } from 'react' -import { LoadingIndicator } from 'web/components/loading-indicator' +import React, { memo, ReactNode, useEffect, useState } from 'react' import { Row } from 'web/components/layout/row' import clsx from 'clsx' import { notification_subscription_types, notification_destination_types, + PrivateUser, } from 'common/user' import { updatePrivateUser } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' @@ -23,21 +22,22 @@ import { UsersIcon, } from '@heroicons/react/outline' import { WatchMarketModal } from 'web/components/contract/watch-market-modal' -import { filterDefined } from 'common/util/array' import toast from 'react-hot-toast' import { SwitchSetting } from 'web/components/switch-setting' +import { uniq } from 'lodash' +import { + storageStore, + usePersistentState, +} from 'web/hooks/use-persistent-state' +import { safeLocalStorage } from 'web/lib/util/local' export function NotificationSettings(props: { navigateToSection: string | undefined + privateUser: PrivateUser }) { - const { navigateToSection } = props - const privateUser = usePrivateUser() + const { navigateToSection, privateUser } = props const [showWatchModal, setShowWatchModal] = useState(false) - if (!privateUser || !privateUser.notificationSubscriptionTypes) { - return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} /> - } - const emailsEnabled: Array<keyof notification_subscription_types> = [ 'all_comments_on_watched_markets', 'all_replies_to_my_comments_on_watched_markets', @@ -165,32 +165,29 @@ export function NotificationSettings(props: { }, } - const NotificationSettingLine = ( - description: string, - key: keyof notification_subscription_types, - value: notification_destination_types[] - ) => { - const previousInAppValue = value.includes('browser') - const previousEmailValue = value.includes('email') + function NotificationSettingLine(props: { + description: string + subscriptionTypeKey: keyof notification_subscription_types + destinations: notification_destination_types[] + }) { + const { description, subscriptionTypeKey, destinations } = props + const previousInAppValue = destinations.includes('browser') + const previousEmailValue = destinations.includes('email') const [inAppEnabled, setInAppEnabled] = useState(previousInAppValue) const [emailEnabled, setEmailEnabled] = useState(previousEmailValue) const loading = 'Changing Notifications Settings' const success = 'Changed Notification Settings!' - const highlight = navigateToSection === key + const highlight = navigateToSection === subscriptionTypeKey - useEffect(() => { - if ( - inAppEnabled !== previousInAppValue || - emailEnabled !== previousEmailValue - ) { - toast.promise( + const changeSetting = (setting: 'browser' | 'email', newValue: boolean) => { + toast + .promise( updatePrivateUser(privateUser.id, { notificationSubscriptionTypes: { ...privateUser.notificationSubscriptionTypes, - [key]: filterDefined([ - inAppEnabled ? 'browser' : undefined, - emailEnabled ? 'email' : undefined, - ]), + [subscriptionTypeKey]: destinations.includes(setting) + ? destinations.filter((d) => d !== setting) + : uniq([...destinations, setting]), }, }), { @@ -199,14 +196,14 @@ export function NotificationSettings(props: { error: 'Error changing notification settings. Try again?', } ) - } - }, [ - inAppEnabled, - emailEnabled, - previousInAppValue, - previousEmailValue, - key, - ]) + .then(() => { + if (setting === 'browser') { + setInAppEnabled(newValue) + } else { + setEmailEnabled(newValue) + } + }) + } return ( <Row @@ -220,17 +217,17 @@ export function NotificationSettings(props: { <span>{description}</span> </Row> <Row className={'gap-4'}> - {!browserDisabled.includes(key) && ( + {!browserDisabled.includes(subscriptionTypeKey) && ( <SwitchSetting checked={inAppEnabled} - onChange={setInAppEnabled} + onChange={(newVal) => changeSetting('browser', newVal)} label={'Web'} /> )} - {emailsEnabled.includes(key) && ( + {emailsEnabled.includes(subscriptionTypeKey) && ( <SwitchSetting checked={emailEnabled} - onChange={setEmailEnabled} + onChange={(newVal) => changeSetting('email', newVal)} label={'Email'} /> )} @@ -246,12 +243,22 @@ export function NotificationSettings(props: { return privateUser.notificationSubscriptionTypes[key] ?? [] } - const Section = (icon: ReactNode, data: sectionData) => { + const Section = memo(function Section(props: { + icon: ReactNode + data: sectionData + }) { + const { icon, data } = props const { label, subscriptionTypeToDescription } = data const expand = navigateToSection && Object.keys(subscriptionTypeToDescription).includes(navigateToSection) - const [expanded, setExpanded] = useState(expand) + + // Not sure how to prevent re-render (and collapse of an open section) + // due to a private user settings change. Just going to persist expanded state here + const [expanded, setExpanded] = usePersistentState(expand ?? false, { + key: 'NotificationsSettingsSection-' + label, + store: storageStore(safeLocalStorage()), + }) // Not working as the default value for expanded, so using a useEffect useEffect(() => { @@ -278,19 +285,19 @@ export function NotificationSettings(props: { )} </Row> <Col className={clsx(expanded ? 'block' : 'hidden', 'gap-2 p-2')}> - {Object.entries(subscriptionTypeToDescription).map(([key, value]) => - NotificationSettingLine( - value, - key as keyof notification_subscription_types, - getUsersSavedPreference( + {Object.entries(subscriptionTypeToDescription).map(([key, value]) => ( + <NotificationSettingLine + subscriptionTypeKey={key as keyof notification_subscription_types} + destinations={getUsersSavedPreference( key as keyof notification_subscription_types - ) - ) - )} + )} + description={value} + /> + ))} </Col> </Col> ) - } + }) return ( <div className={'p-2'}> @@ -302,20 +309,38 @@ export function NotificationSettings(props: { onClick={() => setShowWatchModal(true)} /> </Row> - {Section(<ChatIcon className={'h-6 w-6'} />, comments)} - {Section(<LightBulbIcon className={'h-6 w-6'} />, answers)} - {Section(<TrendingUpIcon className={'h-6 w-6'} />, updates)} - {Section(<UserIcon className={'h-6 w-6'} />, yourMarkets)} + <Section icon={<ChatIcon className={'h-6 w-6'} />} data={comments} /> + <Section + icon={<TrendingUpIcon className={'h-6 w-6'} />} + data={updates} + /> + <Section + icon={<LightBulbIcon className={'h-6 w-6'} />} + data={answers} + /> + <Section icon={<UserIcon className={'h-6 w-6'} />} data={yourMarkets} /> <Row className={'gap-2 text-xl text-gray-700'}> <span>Balance Changes</span> </Row> - {Section(<CurrencyDollarIcon className={'h-6 w-6'} />, bonuses)} - {Section(<CashIcon className={'h-6 w-6'} />, otherBalances)} + <Section + icon={<CurrencyDollarIcon className={'h-6 w-6'} />} + data={bonuses} + /> + <Section + icon={<CashIcon className={'h-6 w-6'} />} + data={otherBalances} + /> <Row className={'gap-2 text-xl text-gray-700'}> <span>General</span> </Row> - {Section(<UsersIcon className={'h-6 w-6'} />, userInteractions)} - {Section(<InboxInIcon className={'h-6 w-6'} />, generalOther)} + <Section + icon={<UsersIcon className={'h-6 w-6'} />} + data={userInteractions} + /> + <Section + icon={<InboxInIcon className={'h-6 w-6'} />} + data={generalOther} + /> <WatchMarketModal open={showWatchModal} setOpen={setShowWatchModal} /> </Col> </div> diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 1e8fbb4d..fcac8601 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -112,6 +112,7 @@ export default function Notifications() { content: ( <NotificationSettings navigateToSection={navigateToSection} + privateUser={privateUser} /> ), }, From 74335f2b0171ddb21acd8c9f1f46a65754b3aaa2 Mon Sep 17 00:00:00 2001 From: Sinclair Chen <abc.sinclair@gmail.com> Date: Tue, 13 Sep 2022 16:16:07 -0700 Subject: [PATCH 5/6] Adjust market modal styles (#875) * Refactor add market modals into one component * Adjust style: stickier search, scroll auto --- web/components/contract-search.tsx | 2 +- web/components/contract-select-modal.tsx | 102 ++++++++++++++++++++++ web/components/editor/market-modal.tsx | 88 +++---------------- web/pages/group/[...slugs]/index.tsx | 106 +++++------------------ 4 files changed, 137 insertions(+), 161 deletions(-) create mode 100644 web/components/contract-select-modal.tsx diff --git a/web/components/contract-search.tsx b/web/components/contract-search.tsx index e4b7f9cf..5bd69057 100644 --- a/web/components/contract-search.tsx +++ b/web/components/contract-search.tsx @@ -200,7 +200,7 @@ export function ContractSearch(props: { } return ( - <Col className="h-full"> + <Col> <ContractSearchControls className={headerClassName} defaultSort={defaultSort} diff --git a/web/components/contract-select-modal.tsx b/web/components/contract-select-modal.tsx new file mode 100644 index 00000000..9e23264a --- /dev/null +++ b/web/components/contract-select-modal.tsx @@ -0,0 +1,102 @@ +import { Contract } from 'common/contract' +import { useState } from 'react' +import { Button } from './button' +import { ContractSearch } from './contract-search' +import { Col } from './layout/col' +import { Modal } from './layout/modal' +import { Row } from './layout/row' +import { LoadingIndicator } from './loading-indicator' + +export function SelectMarketsModal(props: { + title: string + description?: React.ReactNode + open: boolean + setOpen: (open: boolean) => void + submitLabel: (length: number) => string + onSubmit: (contracts: Contract[]) => void | Promise<void> + contractSearchOptions?: Partial<Parameters<typeof ContractSearch>[0]> +}) { + const { + title, + description, + open, + setOpen, + submitLabel, + onSubmit, + contractSearchOptions, + } = props + + const [contracts, setContracts] = useState<Contract[]>([]) + const [loading, setLoading] = useState(false) + + async function addContract(contract: Contract) { + if (contracts.map((c) => c.id).includes(contract.id)) { + setContracts(contracts.filter((c) => c.id !== contract.id)) + } else setContracts([...contracts, contract]) + } + + async function onFinish() { + setLoading(true) + await onSubmit(contracts) + setLoading(false) + setOpen(false) + setContracts([]) + } + + return ( + <Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}> + <Col className="h-[85vh] w-full gap-4 rounded-md bg-white"> + <div className="p-8 pb-0"> + <Row> + <div className={'text-xl text-indigo-700'}>{title}</div> + + {!loading && ( + <Row className="grow justify-end gap-4"> + {contracts.length > 0 && ( + <Button onClick={onFinish} color="indigo"> + {submitLabel(contracts.length)} + </Button> + )} + <Button + onClick={() => { + if (contracts.length > 0) { + setContracts([]) + } else { + setOpen(false) + } + }} + color="gray" + > + {contracts.length > 0 ? 'Reset' : 'Cancel'} + </Button> + </Row> + )} + </Row> + {description} + </div> + + {loading && ( + <div className="w-full justify-center"> + <LoadingIndicator /> + </div> + )} + + <div className="overflow-y-auto sm:px-8"> + <ContractSearch + hideOrderSelector + onContractClick={addContract} + cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} + highlightOptions={{ + contractIds: contracts.map((c) => c.id), + highlightClassName: + '!bg-indigo-100 outline outline-2 outline-indigo-300', + }} + additionalFilter={{}} /* hide pills */ + headerClassName="bg-white" + {...contractSearchOptions} + /> + </div> + </Col> + </Modal> + ) +} diff --git a/web/components/editor/market-modal.tsx b/web/components/editor/market-modal.tsx index 31c437b1..1e2c1482 100644 --- a/web/components/editor/market-modal.tsx +++ b/web/components/editor/market-modal.tsx @@ -1,12 +1,6 @@ import { Editor } from '@tiptap/react' import { Contract } from 'common/contract' -import { useState } from 'react' -import { Button } from '../button' -import { ContractSearch } from '../contract-search' -import { Col } from '../layout/col' -import { Modal } from '../layout/modal' -import { Row } from '../layout/row' -import { LoadingIndicator } from '../loading-indicator' +import { SelectMarketsModal } from '../contract-select-modal' import { embedContractCode, embedContractGridCode } from '../share-embed-button' import { insertContent } from './utils' @@ -17,83 +11,23 @@ export function MarketModal(props: { }) { const { editor, open, setOpen } = props - const [contracts, setContracts] = useState<Contract[]>([]) - const [loading, setLoading] = useState(false) - - async function addContract(contract: Contract) { - if (contracts.map((c) => c.id).includes(contract.id)) { - setContracts(contracts.filter((c) => c.id !== contract.id)) - } else setContracts([...contracts, contract]) - } - - async function doneAddingContracts() { - setLoading(true) + function onSubmit(contracts: Contract[]) { if (contracts.length == 1) { insertContent(editor, embedContractCode(contracts[0])) } else if (contracts.length > 1) { insertContent(editor, embedContractGridCode(contracts)) } - setLoading(false) - setOpen(false) - setContracts([]) } return ( - <Modal open={open} setOpen={setOpen} className={'sm:p-0'} size={'lg'}> - <Col className="h-[85vh] w-full gap-4 rounded-md bg-white"> - <Row className="p-8 pb-0"> - <div className={'text-xl text-indigo-700'}>Embed a market</div> - - {!loading && ( - <Row className="grow justify-end gap-4"> - {contracts.length == 1 && ( - <Button onClick={doneAddingContracts} color={'indigo'}> - Embed 1 question - </Button> - )} - {contracts.length > 1 && ( - <Button onClick={doneAddingContracts} color={'indigo'}> - Embed grid of {contracts.length} question - {contracts.length > 1 && 's'} - </Button> - )} - <Button - onClick={() => { - if (contracts.length > 0) { - setContracts([]) - } else { - setOpen(false) - } - }} - color="gray" - > - {contracts.length > 0 ? 'Reset' : 'Cancel'} - </Button> - </Row> - )} - </Row> - - {loading && ( - <div className="w-full justify-center"> - <LoadingIndicator /> - </div> - )} - - <div className="overflow-y-scroll sm:px-8"> - <ContractSearch - hideOrderSelector - onContractClick={addContract} - cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} - highlightOptions={{ - contractIds: contracts.map((c) => c.id), - highlightClassName: - '!bg-indigo-100 outline outline-2 outline-indigo-300', - }} - additionalFilter={{}} /* hide pills */ - headerClassName="bg-white" - /> - </div> - </Col> - </Modal> + <SelectMarketsModal + title="Embed markets" + open={open} + setOpen={setOpen} + submitLabel={(len) => + len == 1 ? 'Embed 1 question' : `Embed grid of ${len} questions` + } + onSubmit={onSubmit} + /> ) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index f124e225..f1521b42 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -31,8 +31,6 @@ import { SEO } from 'web/components/SEO' import { Linkify } from 'web/components/linkify' import { fromPropz, usePropz } from 'web/hooks/use-propz' import { Tabs } from 'web/components/layout/tabs' -import { LoadingIndicator } from 'web/components/loading-indicator' -import { Modal } from 'web/components/layout/modal' import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' import { ContractSearch } from 'web/components/contract-search' import { JoinOrLeaveGroupButton } from 'web/components/groups/groups-button' @@ -51,6 +49,7 @@ import { Spacer } from 'web/components/layout/spacer' import { usePost } from 'web/hooks/use-post' import { useAdmin } from 'web/hooks/use-admin' import { track } from '@amplitude/analytics-browser' +import { SelectMarketsModal } from 'web/components/contract-select-modal' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -401,27 +400,12 @@ function GroupLeaderboard(props: { function AddContractButton(props: { group: Group; user: User }) { const { group, user } = props const [open, setOpen] = useState(false) - const [contracts, setContracts] = useState<Contract[]>([]) - const [loading, setLoading] = useState(false) const groupContractIds = useGroupContractIds(group.id) - async function addContractToCurrentGroup(contract: Contract) { - if (contracts.map((c) => c.id).includes(contract.id)) { - setContracts(contracts.filter((c) => c.id !== contract.id)) - } else setContracts([...contracts, contract]) - } - - async function doneAddingContracts() { - Promise.all( - contracts.map(async (contract) => { - setLoading(true) - await addContractToGroup(group, contract, user.id) - }) - ).then(() => { - setLoading(false) - setOpen(false) - setContracts([]) - }) + async function onSubmit(contracts: Contract[]) { + await Promise.all( + contracts.map((contract) => addContractToGroup(group, contract, user.id)) + ) } return ( @@ -437,71 +421,27 @@ function AddContractButton(props: { group: Group; user: User }) { </Button> </div> - <Modal + <SelectMarketsModal open={open} setOpen={setOpen} - className={'max-w-4xl sm:p-0'} - size={'xl'} - > - <Col - className={'min-h-screen w-full max-w-4xl gap-4 rounded-md bg-white'} - > - <Col className="p-8 pb-0"> - <div className={'text-xl text-indigo-700'}>Add markets</div> - - <div className={'text-md my-4 text-gray-600'}> - Add pre-existing markets to this group, or{' '} - <Link href={`/create?groupId=${group.id}`}> - <span className="cursor-pointer font-semibold underline"> - create a new one - </span> - </Link> - . - </div> - - {contracts.length > 0 && ( - <Col className={'w-full '}> - {!loading ? ( - <Row className={'justify-end gap-4'}> - <Button onClick={doneAddingContracts} color={'indigo'}> - Add {contracts.length} question - {contracts.length > 1 && 's'} - </Button> - <Button - onClick={() => { - setContracts([]) - }} - color={'gray'} - > - Cancel - </Button> - </Row> - ) : ( - <Row className={'justify-center'}> - <LoadingIndicator /> - </Row> - )} - </Col> - )} - </Col> - - <div className={'overflow-y-scroll sm:px-8'}> - <ContractSearch - user={user} - hideOrderSelector={true} - onContractClick={addContractToCurrentGroup} - cardHideOptions={{ hideGroupLink: true, hideQuickBet: true }} - additionalFilter={{ - excludeContractIds: groupContractIds, - }} - highlightOptions={{ - contractIds: contracts.map((c) => c.id), - highlightClassName: '!bg-indigo-100 border-indigo-100 border-2', - }} - /> + title="Add markets" + description={ + <div className={'text-md my-4 text-gray-600'}> + Add pre-existing markets to this group, or{' '} + <Link href={`/create?groupId=${group.id}`}> + <span className="cursor-pointer font-semibold underline"> + create a new one + </span> + </Link> + . </div> - </Col> - </Modal> + } + submitLabel={(len) => `Add ${len} question${len !== 1 ? 's' : ''}`} + onSubmit={onSubmit} + contractSearchOptions={{ + additionalFilter: { excludeContractIds: groupContractIds }, + }} + /> </> ) } From f6feacfbc9bf69c98345daa7df9c1566db9b8b8d Mon Sep 17 00:00:00 2001 From: Ian Philips <iansphilips@gmail.com> Date: Tue, 13 Sep 2022 17:18:16 -0600 Subject: [PATCH 6/6] Fix lint and persistent storage key --- web/components/notification-settings.tsx | 26 +++++++++++++----------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/web/components/notification-settings.tsx b/web/components/notification-settings.tsx index 61e3b9d9..83ebf894 100644 --- a/web/components/notification-settings.tsx +++ b/web/components/notification-settings.tsx @@ -81,14 +81,14 @@ export function NotificationSettings(props: { 'thank_you_for_purchases', ] - type sectionData = { + type SectionData = { label: string subscriptionTypeToDescription: { [key in keyof Partial<notification_subscription_types>]: string } } - const comments: sectionData = { + const comments: SectionData = { label: 'New Comments', subscriptionTypeToDescription: { all_comments_on_watched_markets: 'All new comments', @@ -102,7 +102,7 @@ export function NotificationSettings(props: { }, } - const answers: sectionData = { + const answers: SectionData = { label: 'New Answers', subscriptionTypeToDescription: { all_answers_on_watched_markets: 'All new answers', @@ -111,7 +111,7 @@ export function NotificationSettings(props: { // answers_by_market_creator_on_watched_markets: 'By market creator', }, } - const updates: sectionData = { + const updates: SectionData = { label: 'Updates & Resolutions', subscriptionTypeToDescription: { market_updates_on_watched_markets: 'All creator updates', @@ -121,7 +121,7 @@ export function NotificationSettings(props: { // probability_updates_on_watched_markets: 'Probability updates', }, } - const yourMarkets: sectionData = { + const yourMarkets: SectionData = { label: 'Markets You Created', subscriptionTypeToDescription: { your_contract_closed: 'Your market has closed (and needs resolution)', @@ -131,7 +131,7 @@ export function NotificationSettings(props: { tips_on_your_markets: 'Likes on your markets', }, } - const bonuses: sectionData = { + const bonuses: SectionData = { label: 'Bonuses', subscriptionTypeToDescription: { betting_streaks: 'Prediction streak bonuses', @@ -139,7 +139,7 @@ export function NotificationSettings(props: { unique_bettors_on_your_contract: 'Unique bettor bonuses on your markets', }, } - const otherBalances: sectionData = { + const otherBalances: SectionData = { label: 'Other', subscriptionTypeToDescription: { loan_income: 'Automatic loans from your profitable bets', @@ -147,7 +147,7 @@ export function NotificationSettings(props: { tips_on_your_comments: 'Tips on your comments', }, } - const userInteractions: sectionData = { + const userInteractions: SectionData = { label: 'Users', subscriptionTypeToDescription: { tagged_user: 'A user tagged you', @@ -155,7 +155,7 @@ export function NotificationSettings(props: { contract_from_followed_user: 'New markets created by users you follow', }, } - const generalOther: sectionData = { + const generalOther: SectionData = { label: 'Other', subscriptionTypeToDescription: { trending_markets: 'Weekly interesting markets', @@ -245,7 +245,7 @@ export function NotificationSettings(props: { const Section = memo(function Section(props: { icon: ReactNode - data: sectionData + data: SectionData }) { const { icon, data } = props const { label, subscriptionTypeToDescription } = data @@ -256,14 +256,16 @@ export function NotificationSettings(props: { // Not sure how to prevent re-render (and collapse of an open section) // due to a private user settings change. Just going to persist expanded state here const [expanded, setExpanded] = usePersistentState(expand ?? false, { - key: 'NotificationsSettingsSection-' + label, + key: + 'NotificationsSettingsSection-' + + Object.keys(subscriptionTypeToDescription).join('-'), store: storageStore(safeLocalStorage()), }) // Not working as the default value for expanded, so using a useEffect useEffect(() => { if (expand) setExpanded(true) - }, [expand]) + }, [expand, setExpanded]) return ( <Col className={clsx('ml-2 gap-2')}>