From 935ff7b97a22fec5ad351c7454127b9d8b8d8b39 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Tue, 4 Oct 2022 16:47:06 -0600 Subject: [PATCH 001/156] Personalized interesting markets emails [WIP] (#1001) * Test new personalized emails in prod - logs only * fix import --- functions/src/test-scheduled-function.ts | 4 +- functions/src/weekly-markets-emails.ts | 331 +++++++++++++++++++---- 2 files changed, 276 insertions(+), 59 deletions(-) diff --git a/functions/src/test-scheduled-function.ts b/functions/src/test-scheduled-function.ts index 41aa9fe9..c4465703 100644 --- a/functions/src/test-scheduled-function.ts +++ b/functions/src/test-scheduled-function.ts @@ -1,6 +1,6 @@ import { APIError, newEndpoint } from './api' -import { sendPortfolioUpdateEmailsToAllUsers } from './weekly-portfolio-emails' import { isProd } from './utils' +import { sendTrendingMarketsEmailsToAllUsers } from 'functions/src/weekly-markets-emails' // Function for testing scheduled functions locally export const testscheduledfunction = newEndpoint( @@ -10,7 +10,7 @@ export const testscheduledfunction = newEndpoint( throw new APIError(400, 'This function is only available in dev mode') // Replace your function here - await sendPortfolioUpdateEmailsToAllUsers() + await sendTrendingMarketsEmailsToAllUsers() return { success: true } } diff --git a/functions/src/weekly-markets-emails.ts b/functions/src/weekly-markets-emails.ts index 68063f95..5d338a79 100644 --- a/functions/src/weekly-markets-emails.ts +++ b/functions/src/weekly-markets-emails.ts @@ -2,24 +2,18 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { Contract } from '../../common/contract' -import { - getAllPrivateUsers, - getPrivateUser, - getUser, - getValues, - isProd, - log, -} from './utils' -import { sendInterestingMarketsEmail } from './emails' +import { getGroup, getPrivateUser, getUser, getValues, log } from './utils' import { createRNG, shuffle } from '../../common/util/random' -import { DAY_MS } from '../../common/util/time' +import { DAY_MS, HOUR_MS } from '../../common/util/time' import { filterDefined } from '../../common/util/array' +import { Follow } from '../../common/follow' +import { countBy, uniqBy } from 'lodash' +import { sendInterestingMarketsEmail } from './emails' export const weeklyMarketsEmails = functions .runWith({ secrets: ['MAILGUN_KEY'], memory: '4GB' }) - // TODO change back to Monday after the rest of the emails go out - // every minute on Tuesday for 2 hours starting at 12pm PT (UTC -07:00) - .pubsub.schedule('* 19-20 * * 2') + // every minute on Monday for 2 hours starting at 12pm PT (UTC -07:00) + .pubsub.schedule('* 19-20 * * 1') .timeZone('Etc/UTC') .onRun(async () => { await sendTrendingMarketsEmailsToAllUsers() @@ -41,20 +35,30 @@ export async function getTrendingContracts() { ) } -async function sendTrendingMarketsEmailsToAllUsers() { +export async function sendTrendingMarketsEmailsToAllUsers() { const numContractsToSend = 6 - const privateUsers = isProd() - ? await getAllPrivateUsers() - : filterDefined([await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2')]) - // get all users that haven't unsubscribed from weekly emails - const privateUsersToSendEmailsTo = privateUsers - .filter((user) => { - return ( - user.notificationPreferences.trending_markets.includes('email') && - !user.weeklyTrendingEmailSent - ) - }) - .slice(125) // Send the emails out in batches + // const privateUsers = + // isProd() + // ? await getAllPrivateUsers() + // filterDefined([ + // await getPrivateUser('6hHpzvRG0pMq8PNJs7RZj2qlZGn2'), // dev Ian + // ]) + const privateUsersToSendEmailsTo = + // get all users that haven't unsubscribed from weekly emails + // isProd() + // ? privateUsers + // .filter((user) => { + // user.notificationPreferences.trending_markets.includes('email') && + // !user.weeklyTrendingEmailSent + // }) + // .slice(125) // Send the emails out in batches + // : + // privateUsers + filterDefined([ + await getPrivateUser('AJwLWoo3xue32XIiAVrL5SyR1WB2'), // prod Ian + await getPrivateUser('FptiiMZZ6dQivihLI8MYFQ6ypSw1'), + ]) + log( 'Sending weekly trending emails to', privateUsersToSendEmailsTo.length, @@ -72,41 +76,254 @@ async function sendTrendingMarketsEmailsToAllUsers() { !contract.groupSlugs?.includes('manifold-6748e065087e') ) .slice(0, 20) - log( - `Found ${trendingContracts.length} trending contracts:\n`, - trendingContracts.map((c) => c.question).join('\n ') - ) + // log( + // `Found ${trendingContracts.length} trending contracts:\n`, + // trendingContracts.map((c) => c.question).join('\n ') + // ) - // TODO: convert to Promise.all - for (const privateUser of privateUsersToSendEmailsTo) { - if (!privateUser.email) { - log(`No email for ${privateUser.username}`) - continue - } - const contractsAvailableToSend = trendingContracts.filter((contract) => { - return !contract.uniqueBettorIds?.includes(privateUser.id) - }) - if (contractsAvailableToSend.length < numContractsToSend) { - log('not enough new, unbet-on contracts to send to user', privateUser.id) - await firestore.collection('private-users').doc(privateUser.id).update({ + await Promise.all( + privateUsersToSendEmailsTo.map(async (privateUser) => { + if (!privateUser.email) { + log(`No email for ${privateUser.username}`) + return + } + const marketsAvailableToSend = uniqBy( + [ + ...(await getUserUnBetOnFollowsMarkets( + privateUser.id, + privateUser.id + )), + ...(await getUserUnBetOnGroupsMarkets(privateUser.id)), + ...(await getSimilarBettorsMarkets(privateUser.id)), + ], + (contract) => contract.id + ) + // at least send them trending contracts if nothing else + if (marketsAvailableToSend.length < numContractsToSend) + marketsAvailableToSend.push( + ...trendingContracts + .filter( + (contract) => + !contract.uniqueBettorIds?.includes(privateUser.id) && + !marketsAvailableToSend.map((c) => c.id).includes(contract.id) + ) + .slice(0, numContractsToSend - marketsAvailableToSend.length) + ) + + if (marketsAvailableToSend.length < numContractsToSend) { + log( + 'not enough new, unbet-on contracts to send to user', + privateUser.id + ) + await firestore.collection('private-users').doc(privateUser.id).update({ + weeklyTrendingEmailSent: true, + }) + return + } + // choose random subset of contracts to send to user + const contractsToSend = chooseRandomSubset( + marketsAvailableToSend, + numContractsToSend + ) + + const user = await getUser(privateUser.id) + if (!user) return + + console.log( + 'sending contracts:', + contractsToSend.map((c) => [c.question, c.popularityScore]) + ) + // if they don't have enough markets, find user bets and get the other bettor ids who most overlap on those markets, then do the same thing as above for them + // await sendInterestingMarketsEmail(user, privateUser, contractsToSend) + await sendInterestingMarketsEmail( + user, + privateUsersToSendEmailsTo[0], + contractsToSend + ) + await firestore.collection('private-users').doc(user.id).update({ weeklyTrendingEmailSent: true, }) - continue - } - // choose random subset of contracts to send to user - const contractsToSend = chooseRandomSubset( - contractsAvailableToSend, - numContractsToSend - ) - - const user = await getUser(privateUser.id) - if (!user) continue - - await sendInterestingMarketsEmail(user, privateUser, contractsToSend) - await firestore.collection('private-users').doc(user.id).update({ - weeklyTrendingEmailSent: true, }) - } + ) +} + +// TODO: figure out a good minimum popularity score to filter by +const MINIMUM_POPULARITY_SCORE = 2 + +const getUserUnBetOnFollowsMarkets = async ( + userId: string, + unBetOnByUserId: string +) => { + const follows = await getValues( + firestore.collection('users').doc(userId).collection('follows') + ) + console.log( + 'follows', + follows.map((f) => f.userId) + ) + + const unBetOnContractsFromFollows = await Promise.all( + follows.map(async (follow) => { + const unresolvedContracts = await getValues( + firestore + .collection('contracts') + .where('isResolved', '==', false) + .where('visibility', '==', 'public') + .where('creatorId', '==', follow.userId) + // can't use multiple inequality (/orderBy) operators on different fields, + // so have to filter for closed contracts separately + .orderBy('popularityScore', 'desc') + .limit(50) + ) + // filter out contracts that have close times less than 6 hours from now + const openContracts = unresolvedContracts.filter( + (contract) => (contract?.closeTime ?? 0) > Date.now() + 6 * HOUR_MS + ) + + return openContracts.filter( + (contract) => !contract.uniqueBettorIds?.includes(unBetOnByUserId) + ) + }) + ) + + const sortedMarkets = unBetOnContractsFromFollows + .flat() + .filter( + (contract) => + contract.popularityScore !== undefined && + contract.popularityScore > MINIMUM_POPULARITY_SCORE + ) + .sort((a, b) => (b.popularityScore ?? 0) - (a.popularityScore ?? 0)) + console.log( + 'sorted top 10 follow Markets', + sortedMarkets + .slice(0, 10) + .map((c) => [c.question, c.popularityScore, c.creatorId]) + ) + return sortedMarkets +} + +const getUserUnBetOnGroupsMarkets = async (userId: string) => { + const snap = await firestore + .collectionGroup('groupMembers') + .where('userId', '==', userId) + .get() + + const groupIds = filterDefined( + snap.docs.map((doc) => doc.ref.parent.parent?.id) + ) + const groups = filterDefined( + await Promise.all(groupIds.map(async (groupId) => await getGroup(groupId))) + ) + console.log( + 'groups', + groups.map((g) => g.name) + ) + const unBetOnContractsFromGroups = await Promise.all( + groups.map(async (group) => { + const unresolvedContracts = await getValues( + firestore + .collection('contracts') + .where('isResolved', '==', false) + .where('visibility', '==', 'public') + .where('groupSlugs', 'array-contains', group.slug) + // can't use multiple inequality (/orderBy) operators on different fields, + // so have to filter for closed contracts separately + .orderBy('popularityScore', 'desc') + .limit(50) + ) + // filter out contracts that have close times less than 6 hours from now + const openContracts = unresolvedContracts.filter( + (contract) => (contract?.closeTime ?? 0) > Date.now() + 6 * HOUR_MS + ) + + return openContracts.filter( + (contract) => !contract.uniqueBettorIds?.includes(userId) + ) + }) + ) + const sortedMarkets = unBetOnContractsFromGroups + .flat() + .filter( + (contract) => + contract.popularityScore !== undefined && + contract.popularityScore > MINIMUM_POPULARITY_SCORE + ) + .sort((a, b) => (b.popularityScore ?? 0) - (a.popularityScore ?? 0)) + console.log( + 'top 10 sorted group Markets', + sortedMarkets + .slice(0, 10) + .map((c) => [c.question, c.popularityScore, c.groupSlugs]) + ) + return sortedMarkets +} + +// Gets markets followed by similar bettors and bet on by similar bettors +const getSimilarBettorsMarkets = async (userId: string) => { + // get contracts with unique bettor ids with this user + const contractsUserHasBetOn = await getValues( + firestore + .collection('contracts') + .where('uniqueBettorIds', 'array-contains', userId) + ) + // count the number of times each unique bettor id appears on those contracts + const bettorIdsToCounts = countBy( + contractsUserHasBetOn.map((contract) => contract.uniqueBettorIds).flat(), + (bettorId) => bettorId + ) + console.log('bettorIdCounts', bettorIdsToCounts) + + // sort by number of times they appear with at least 2 appearances + const sortedBettorIds = Object.entries(bettorIdsToCounts) + .sort((a, b) => b[1] - a[1]) + .filter((bettorId) => bettorId[1] > 2) + .map((entry) => entry[0]) + .filter((bettorId) => bettorId !== userId) + + // get the top 10 most similar bettors (excluding this user) + const similarBettorIds = sortedBettorIds.slice(0, 10) + console.log('sortedBettorIds', sortedBettorIds) + // get their followed users' markets + const followedUsersMarkets = ( + await Promise.all( + similarBettorIds.map(async (bettorId) => + getUserUnBetOnFollowsMarkets(bettorId, userId) + ) + ) + ).flat() + console.log( + 'top 10 followedUsersMarkets', + followedUsersMarkets.map((c) => [c.question, c.creatorId]).slice(0, 10) + ) + + // get contracts with unique bettor ids with this user + const contractsSimilarBettorsHaveBetOn = ( + await getValues( + firestore + .collection('contracts') + .where( + 'uniqueBettorIds', + 'array-contains-any', + sortedBettorIds.slice(0, 10) + ) + .orderBy('popularityScore', 'desc') + .limit(100) + ) + ).filter((contract) => !contract.uniqueBettorIds?.includes(userId)) + console.log( + 'top 10 contractsSimilarBettorsHaveBetOn', + contractsSimilarBettorsHaveBetOn + .map((c) => [ + c.question, + c.uniqueBettorIds?.filter((bid) => similarBettorIds.includes(bid)), + ]) + .slice(0, 10) + ) + + return [...followedUsersMarkets, ...contractsSimilarBettorsHaveBetOn].sort( + (a, b) => (b?.popularityScore ?? 0) - (a?.popularityScore ?? 0) + ) } const fiveMinutes = 5 * 60 * 1000 From 7b9aeea0bd68d97e5806a21eec744cd894d10e36 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Tue, 4 Oct 2022 17:12:07 -0600 Subject: [PATCH 002/156] Ignore similar bettor's followed user's markets --- functions/src/weekly-markets-emails.ts | 46 +++++++++++++++----------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/functions/src/weekly-markets-emails.ts b/functions/src/weekly-markets-emails.ts index 5d338a79..4405c9a5 100644 --- a/functions/src/weekly-markets-emails.ts +++ b/functions/src/weekly-markets-emails.ts @@ -283,19 +283,7 @@ const getSimilarBettorsMarkets = async (userId: string) => { // get the top 10 most similar bettors (excluding this user) const similarBettorIds = sortedBettorIds.slice(0, 10) - console.log('sortedBettorIds', sortedBettorIds) - // get their followed users' markets - const followedUsersMarkets = ( - await Promise.all( - similarBettorIds.map(async (bettorId) => - getUserUnBetOnFollowsMarkets(bettorId, userId) - ) - ) - ).flat() - console.log( - 'top 10 followedUsersMarkets', - followedUsersMarkets.map((c) => [c.question, c.creatorId]).slice(0, 10) - ) + console.log('top sortedBettorIds', similarBettorIds) // get contracts with unique bettor ids with this user const contractsSimilarBettorsHaveBetOn = ( @@ -305,15 +293,37 @@ const getSimilarBettorsMarkets = async (userId: string) => { .where( 'uniqueBettorIds', 'array-contains-any', - sortedBettorIds.slice(0, 10) + similarBettorIds.slice(0, 10) ) .orderBy('popularityScore', 'desc') .limit(100) ) ).filter((contract) => !contract.uniqueBettorIds?.includes(userId)) - console.log( - 'top 10 contractsSimilarBettorsHaveBetOn', + + // sort the contracts by how many times similar bettor ids are in their unique bettor ids array + const sortedContractsToAppearancesInSimilarBettorsBets = contractsSimilarBettorsHaveBetOn + .map((contract) => { + const appearances = contract.uniqueBettorIds?.filter((bettorId) => + similarBettorIds.includes(bettorId) + ).length + return [contract, appearances] as [Contract, number] + }) + .sort((a, b) => b[1] - a[1]) + console.log( + 'sortedContractsToAppearancesInSimilarBettorsBets', + sortedContractsToAppearancesInSimilarBettorsBets.map((c) => [ + c[0].question, + c[1], + ]) + ) + + const topMostSimilarContracts = + sortedContractsToAppearancesInSimilarBettorsBets.map((entry) => entry[0]) + + console.log( + 'top 10 sortedContractsToAppearancesInSimilarBettorsBets', + topMostSimilarContracts .map((c) => [ c.question, c.uniqueBettorIds?.filter((bid) => similarBettorIds.includes(bid)), @@ -321,9 +331,7 @@ const getSimilarBettorsMarkets = async (userId: string) => { .slice(0, 10) ) - return [...followedUsersMarkets, ...contractsSimilarBettorsHaveBetOn].sort( - (a, b) => (b?.popularityScore ?? 0) - (a?.popularityScore ?? 0) - ) + return topMostSimilarContracts } const fiveMinutes = 5 * 60 * 1000 From 40b07329bd72092a70a1f6e28f589e9d2ac272ee Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Tue, 4 Oct 2022 16:42:17 -0700 Subject: [PATCH 003/156] Make follow & unfollow buttons same size --- web/components/groups/groups-button.tsx | 34 ++++++++++++------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index 5c9d2edd..814efd81 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -13,6 +13,7 @@ import { joinGroup, leaveGroup } from 'web/lib/firebase/groups' import { firebaseLogin } from 'web/lib/firebase/users' import { GroupLinkItem } from 'web/pages/groups' import toast from 'react-hot-toast' +import { Button } from '../button' export function GroupsButton(props: { user: User; className?: string }) { const { user, className } = props @@ -92,23 +93,22 @@ export function JoinOrLeaveGroupButton(props: { group: Group isMember: boolean user: User | undefined | null - small?: boolean className?: string }) { - const { group, small, className, isMember, user } = props - const smallStyle = - 'btn !btn-xs border-2 border-gray-500 bg-white normal-case text-gray-500 hover:border-gray-500 hover:bg-white hover:text-gray-500' + const { group, className, isMember, user } = props if (!user) { if (!group.anyoneCanJoin) return
Closed
return ( - + ) } const onJoinGroup = () => { @@ -124,27 +124,27 @@ export function JoinOrLeaveGroupButton(props: { if (isMember) { return ( - + ) } if (!group.anyoneCanJoin) return
Closed
return ( - + ) } From 3f0b6657533013e66e6f895c8ac8f85956bd7b1e Mon Sep 17 00:00:00 2001 From: Sinclair Chen Date: Tue, 4 Oct 2022 16:57:05 -0700 Subject: [PATCH 004/156] Add Mriya to charities list --- common/charity.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/common/charity.ts b/common/charity.ts index fd5abc36..0ebeeec1 100644 --- a/common/charity.ts +++ b/common/charity.ts @@ -576,7 +576,7 @@ Work towards sustainable, systemic change.`, 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', @@ -589,6 +589,14 @@ CaRLA uses legal advocacy and education to ensure all cities comply with their o 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) => { const slug = charity.name.toLowerCase().replace(/\s/g, '-') return { From f551e6c4694f2161893e9060f22c420286d0d716 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Tue, 4 Oct 2022 19:06:59 -0500 Subject: [PATCH 005/156] market close styling --- web/components/contract/contract-details.tsx | 24 ++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 63b8c617..3b308667 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -437,14 +437,14 @@ function EditableCloseDate(props: { return ( <> - - + + + + - From 8043fa515a28f9607ef60d5bb870223043559a61 Mon Sep 17 00:00:00 2001 From: James Grugett Date: Tue, 4 Oct 2022 19:10:43 -0500 Subject: [PATCH 006/156] Show loan repaid in sell dialog --- web/components/bet-panel.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 6fcfc899..90db558d 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -838,6 +838,11 @@ export function SellPanel(props: { const sellQuantity = isSellingAllShares ? shares : amount + const loanAmount = sumBy(userBets, (bet) => bet.loanAmount ?? 0) + const soldShares = Math.min(sellQuantity ?? 0, shares) + const saleFrac = soldShares / shares + const loanPaid = saleFrac * loanAmount + async function submitSell() { if (!user || !amount) return @@ -882,6 +887,7 @@ export function SellPanel(props: { sharesOutcome, unfilledBets ) + const netProceeds = saleValue - loanPaid const resultProb = getCpmmProbability(cpmmState.pool, cpmmState.p) const getValue = getMappedValue(contract) @@ -941,9 +947,21 @@ export function SellPanel(props: { - Sale proceeds + Sale amount {formatMoney(saleValue)} + {loanPaid !== 0 && ( + <> + + Loan repaid + {formatMoney(-loanPaid)} + + + Net proceeds + {formatMoney(netProceeds)} + + + )}
{isPseudoNumeric ? 'Estimated value' : 'Probability'} @@ -960,7 +978,7 @@ export function SellPanel(props: { Date: Tue, 4 Oct 2022 19:35:29 -0500 Subject: [PATCH 007/156] Add profit amount to sell dialog. --- web/components/bet-panel.tsx | 38 ++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 90db558d..040a0406 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -25,7 +25,7 @@ import { NoLabel, YesLabel, } from './outcome-label' -import { getProbability } from 'common/calculate' +import { getContractBetMetrics, getProbability } from 'common/calculate' import { useFocus } from 'web/hooks/use-focus' import { useUserContractBets } from 'web/hooks/use-user-bets' import { calculateCpmmSale, getCpmmProbability } from 'common/calculate-cpmm' @@ -843,6 +843,9 @@ export function SellPanel(props: { const saleFrac = soldShares / shares const loanPaid = saleFrac * loanAmount + const { invested } = getContractBetMetrics(contract, userBets) + const costBasis = invested * saleFrac + async function submitSell() { if (!user || !amount) return @@ -888,6 +891,7 @@ export function SellPanel(props: { unfilledBets ) const netProceeds = saleValue - loanPaid + const profit = saleValue - costBasis const resultProb = getCpmmProbability(cpmmState.pool, cpmmState.p) const getValue = getMappedValue(contract) @@ -950,18 +954,10 @@ export function SellPanel(props: { Sale amount {formatMoney(saleValue)} - {loanPaid !== 0 && ( - <> - - Loan repaid - {formatMoney(-loanPaid)} - - - Net proceeds - {formatMoney(netProceeds)} - - - )} + + Profit + {formatMoney(profit)} +
{isPseudoNumeric ? 'Estimated value' : 'Probability'} @@ -972,20 +968,32 @@ export function SellPanel(props: { {format(resultProb)}
+ {loanPaid !== 0 && ( + <> + + Loan repaid + {formatMoney(-loanPaid)} + + + Net proceeds + {formatMoney(netProceeds)} + + + )} {wasSubmitted &&
Sell submitted!
} From 7a271fce2973590165eec1eb17ed36551adc7044 Mon Sep 17 00:00:00 2001 From: mantikoros Date: Tue, 4 Oct 2022 19:35:44 -0500 Subject: [PATCH 008/156] fix button --- web/components/answers/answer-bet-panel.tsx | 1 + web/components/bet-panel.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/web/components/answers/answer-bet-panel.tsx b/web/components/answers/answer-bet-panel.tsx index 9867abab..cbe7bc1f 100644 --- a/web/components/answers/answer-bet-panel.tsx +++ b/web/components/answers/answer-bet-panel.tsx @@ -192,6 +192,7 @@ export function AnswerBetPanel(props: { isSubmitting={isSubmitting} disabled={!!betDisabled} color={'indigo'} + actionLabel="Buy" /> ) : ( diff --git a/web/components/bet-panel.tsx b/web/components/bet-panel.tsx index 040a0406..5af6929c 100644 --- a/web/components/bet-panel.tsx +++ b/web/components/bet-panel.tsx @@ -395,6 +395,7 @@ export function BuyPanel(props: { disabled={!!betDisabled || outcome === undefined} size="xl" color={outcome === 'NO' ? 'red' : 'green'} + actionLabel="Wager" /> )}
)} diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index b9387a03..7111e88f 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -1,11 +1,14 @@ +import React, { memo, useEffect, useRef, useState } from 'react' +import { Editor } from '@tiptap/react' +import { useRouter } from 'next/router' +import { sum } from 'lodash' +import clsx from 'clsx' + import { ContractComment } from 'common/comment' import { Contract } from 'common/contract' -import React, { useEffect, useRef, useState } from 'react' import { useUser } from 'web/hooks/use-user' import { formatMoney } from 'common/util/format' -import { useRouter } from 'next/router' import { Row } from 'web/components/layout/row' -import clsx from 'clsx' import { Avatar } from 'web/components/avatar' import { OutcomeLabel } from 'web/components/outcome-label' import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time' @@ -14,9 +17,9 @@ import { createCommentOnContract } from 'web/lib/firebase/comments' import { Col } from 'web/components/layout/col' import { track } from 'web/lib/service/analytics' import { Tipper } from '../tipper' -import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' +import { CommentTipMap } from 'web/hooks/use-tip-txns' +import { useEvent } from 'web/hooks/use-event' import { Content } from '../editor' -import { Editor } from '@tiptap/react' import { UserLink } from 'web/components/user-link' import { CommentInput } from '../comment-input' import { AwardBountyButton } from 'web/components/award-bounty-button' @@ -32,6 +35,12 @@ export function FeedCommentThread(props: { const { contract, threadComments, tips, parentComment } = props const [replyTo, setReplyTo] = useState() + const user = useUser() + const onSubmitComment = useEvent(() => setReplyTo(undefined)) + const onReplyClick = useEvent((comment: ContractComment) => { + setReplyTo({ id: comment.id, username: comment.userUsername }) + }) + return ( - setReplyTo({ id: comment.id, username: comment.userUsername }) - } + myTip={user ? tips[comment.id]?.[user.id] : undefined} + totalTip={sum(Object.values(tips[comment.id] ?? {}))} + showTip={true} + onReplyClick={onReplyClick} /> ))} {replyTo && ( @@ -60,7 +69,7 @@ export function FeedCommentThread(props: { contract={contract} parentCommentId={parentComment.id} replyTo={replyTo} - onSubmitComment={() => setReplyTo(undefined)} + onSubmitComment={onSubmitComment} /> )} @@ -68,14 +77,17 @@ export function FeedCommentThread(props: { ) } -export function FeedComment(props: { +export const FeedComment = memo(function FeedComment(props: { contract: Contract comment: ContractComment - tips?: CommentTips + showTip?: boolean + myTip?: number + totalTip?: number indent?: boolean - onReplyClick?: () => void + onReplyClick?: (comment: ContractComment) => void }) { - const { contract, comment, tips, indent, onReplyClick } = props + const { contract, comment, myTip, totalTip, showTip, indent, onReplyClick } = + props const { text, content, @@ -180,12 +192,18 @@ export function FeedComment(props: { {onReplyClick && ( )} - {tips && } + {showTip && ( + + )} {(contract.openCommentBounties ?? 0) > 0 && ( )} @@ -193,7 +211,7 @@ export function FeedComment(props: {
) -} +}) function CommentStatus(props: { contract: Contract diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index b201f946..ac978f81 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -1,10 +1,9 @@ import { useEffect, useRef, useState } from 'react' import toast from 'react-hot-toast' -import { debounce, sum } from 'lodash' +import { debounce } from 'lodash' import { Comment } from 'common/comment' import { User } from 'common/user' -import { CommentTips } from 'web/hooks/use-tip-txns' import { useUser } from 'web/hooks/use-user' import { transact } from 'web/lib/firebase/api' import { track } from 'web/lib/service/analytics' @@ -13,25 +12,27 @@ import { Row } from './layout/row' import { LIKE_TIP_AMOUNT } from 'common/like' import { formatMoney } from 'common/util/format' -export function Tipper(prop: { comment: Comment; tips: CommentTips }) { - const { comment, tips } = prop +export function Tipper(prop: { + comment: Comment + myTip: number + totalTip: number +}) { + const { comment, myTip, totalTip } = prop const me = useUser() - const myId = me?.id ?? '' - const savedTip = tips[myId] ?? 0 - const [localTip, setLocalTip] = useState(savedTip) + const [localTip, setLocalTip] = useState(myTip) // listen for user being set const initialized = useRef(false) useEffect(() => { - if (tips[myId] && !initialized.current) { - setLocalTip(tips[myId]) + if (myTip && !initialized.current) { + setLocalTip(myTip) initialized.current = true } - }, [tips, myId]) + }, [myTip]) - const total = sum(Object.values(tips)) - savedTip + localTip + const total = totalTip - myTip + localTip // declare debounced function only on first render const [saveTip] = useState(() => @@ -73,7 +74,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { const addTip = (delta: number) => { setLocalTip(localTip + delta) - me && saveTip(me, comment, localTip - savedTip + delta) + me && saveTip(me, comment, localTip - myTip + delta) toast(`You tipped ${comment.userName} ${formatMoney(LIKE_TIP_AMOUNT)}!`) } diff --git a/web/posts/post-comments.tsx b/web/posts/post-comments.tsx index 74fbb300..a9a8532e 100644 --- a/web/posts/post-comments.tsx +++ b/web/posts/post-comments.tsx @@ -1,5 +1,6 @@ import { track } from '@amplitude/analytics-browser' import { Editor } from '@tiptap/core' +import { sum } from 'lodash' import clsx from 'clsx' import { PostComment } from 'common/comment' import { Post } from 'common/post' @@ -109,6 +110,7 @@ export function PostComment(props: { const { text, content, userUsername, userName, userAvatarUrl, createdTime } = comment + const me = useUser() const [highlighted, setHighlighted] = useState(false) const router = useRouter() useEffect(() => { @@ -162,7 +164,11 @@ export function PostComment(props: { Reply )} - + From a9d5dd7fc8639ab8b8e16c12f66719cba478351a Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Tue, 4 Oct 2022 23:17:09 -0700 Subject: [PATCH 017/156] Memoize `FeedBet` component (#999) --- web/components/feed/feed-bets.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/web/components/feed/feed-bets.tsx b/web/components/feed/feed-bets.tsx index b2852739..900265cb 100644 --- a/web/components/feed/feed-bets.tsx +++ b/web/components/feed/feed-bets.tsx @@ -1,3 +1,4 @@ +import React, { memo, useEffect } from 'react' import dayjs from 'dayjs' import { Contract } from 'common/contract' import { Bet } from 'common/bet' @@ -8,7 +9,6 @@ import clsx from 'clsx' import { formatMoney, formatPercent } from 'common/util/format' import { OutcomeLabel } from 'web/components/outcome-label' import { RelativeTimestamp } from 'web/components/relative-timestamp' -import React, { useEffect } from 'react' import { formatNumericProbability } from 'common/pseudo-numeric' import { SiteLink } from 'web/components/site-link' import { getChallenge, getChallengeUrl } from 'web/lib/firebase/challenges' @@ -16,7 +16,10 @@ import { Challenge } from 'common/challenge' import { UserLink } from 'web/components/user-link' import { BETTOR } from 'common/user' -export function FeedBet(props: { contract: Contract; bet: Bet }) { +export const FeedBet = memo(function FeedBet(props: { + contract: Contract + bet: Bet +}) { const { contract, bet } = props const { userAvatarUrl, userUsername, createdTime } = bet const showUser = dayjs(createdTime).isAfter('2022-06-01') @@ -36,7 +39,7 @@ export function FeedBet(props: { contract: Contract; bet: Bet }) { /> ) -} +}) export function BetStatusText(props: { contract: Contract From 49e97ddac1873012e930caf35bb2a3cc215a03be Mon Sep 17 00:00:00 2001 From: Marshall Polaris Date: Wed, 5 Oct 2022 00:23:23 -0700 Subject: [PATCH 018/156] 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 --- package.json | 3 - web/components/arrange-home.tsx | 2 +- web/components/contract/contracts-grid.tsx | 2 +- web/package.json | 9 +- yarn.lock | 128 +++++++++++---------- 5 files changed, 72 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index dd60d92b..dd721db6 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,5 @@ "prettier": "2.7.1", "ts-node": "10.9.1", "typescript": "4.8.2" - }, - "resolutions": { - "@types/react": "17.0.43" } } diff --git a/web/components/arrange-home.tsx b/web/components/arrange-home.tsx index 45c8a588..4953fc31 100644 --- a/web/components/arrange-home.tsx +++ b/web/components/arrange-home.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx' -import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' +import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd' import { MenuIcon } from '@heroicons/react/solid' import { toast } from 'react-hot-toast' diff --git a/web/components/contract/contracts-grid.tsx b/web/components/contract/contracts-grid.tsx index d6206766..7aef2282 100644 --- a/web/components/contract/contracts-grid.tsx +++ b/web/components/contract/contracts-grid.tsx @@ -45,7 +45,7 @@ export function ContractsGrid(props: { cardUIOptions || {} const { itemIds: contractIds, highlightClassName } = highlightOptions || {} const onVisibilityUpdated = useCallback( - (visible) => { + (visible: boolean) => { if (visible && loadMore) { loadMore() } diff --git a/web/package.json b/web/package.json index 02415acb..93ec7ee5 100644 --- a/web/package.json +++ b/web/package.json @@ -22,6 +22,7 @@ "@amplitude/analytics-browser": "0.4.1", "@floating-ui/react-dom-interactions": "0.9.2", "@headlessui/react": "1.6.1", + "@hello-pangea/dnd": "16.0.0", "@heroicons/react": "1.0.6", "@react-query-firebase/firestore": "0.4.2", "@tiptap/core": "2.0.0-beta.182", @@ -40,8 +41,8 @@ "d3-axis": "3.0.0", "d3-brush": "3.0.0", "d3-scale": "4.0.2", - "d3-shape": "3.1.0", "d3-selection": "3.0.0", + "d3-shape": "3.1.0", "daisyui": "1.16.4", "dayjs": "1.10.7", "firebase": "9.9.3", @@ -53,7 +54,6 @@ "node-fetch": "3.2.4", "prosemirror-state": "1.4.1", "react": "18.2.0", - "react-beautiful-dnd": "13.1.1", "react-confetti": "6.0.1", "react-dom": "18.2.0", "react-expanding-textarea": "2.3.6", @@ -72,9 +72,8 @@ "@types/d3": "7.4.0", "@types/lodash": "4.14.178", "@types/node": "16.11.11", - "@types/react": "17.0.43", - "@types/react-beautiful-dnd": "13.1.2", - "@types/react-dom": "17.0.2", + "@types/react": "18.0.21", + "@types/react-dom": "18.0.6", "@types/string-similarity": "^4.0.0", "autoprefixer": "10.2.6", "critters": "0.0.16", diff --git a/yarn.lock b/yarn.lock index 77d855d1..4d57c590 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1310,7 +1310,14 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.15.4", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.18.9": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.0.tgz#22b11c037b094d27a8a2504ea4dcff00f50e2259" + integrity sha512-eR8Lo9hnDS7tqkO7NsV+mKvCmv5boaXFSZ70DnfhcgiEne8hv9oCEd36Klw74EtizEqLsy4YnW8UWwpBVolHZA== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.9.2": version "7.18.9" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== @@ -2351,6 +2358,19 @@ resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.6.1.tgz#d822792e589aac005462491dd62f86095e0c3bef" integrity sha512-gMd6uIs1U4Oz718Z5gFoV0o/vD43/4zvbyiJN9Dt7PK9Ubxn+TmJwTmYwyNJc5KxxU1t0CmgTNgwZX9+4NjCnQ== +"@hello-pangea/dnd@16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@hello-pangea/dnd/-/dnd-16.0.0.tgz#b97791286395924ffbdb4cd0f27f06f2985766d5" + integrity sha512-FprEzwrGMvyclVf8pWTrPbUV7/ZFt6NmL76ePj1mMyZG195htDUkmvET6CBwKJTXmV+AE/GyK4Lv3wpCqrlY/g== + dependencies: + "@babel/runtime" "^7.18.9" + css-box-model "^1.2.1" + memoize-one "^6.0.0" + raf-schd "^4.0.3" + react-redux "^8.0.2" + redux "^4.2.0" + use-memo-one "^1.1.2" + "@heroicons/react@1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@heroicons/react/-/react-1.0.6.tgz#35dd26987228b39ef2316db3b1245c42eb19e324" @@ -3393,7 +3413,7 @@ resolved "https://registry.yarnpkg.com/@types/hogan.js/-/hogan.js-3.0.1.tgz#64c54407b30da359763e14877f5702b8ae85d61c" integrity sha512-D03i/2OY7kGyMq9wdQ7oD8roE49z/ZCZThe/nbahtvuqCNZY9T2MfedOWyeBdbEpY2W8Gnh/dyJLdFtUCOkYbg== -"@types/hoist-non-react-statics@^3.3.0": +"@types/hoist-non-react-statics@^3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== @@ -3521,30 +3541,13 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== -"@types/react-beautiful-dnd@13.1.2": - version "13.1.2" - resolved "https://registry.yarnpkg.com/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz#510405abb09f493afdfd898bf83995dc6385c130" - integrity sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg== +"@types/react-dom@18.0.6": + version "18.0.6" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.6.tgz#36652900024842b74607a17786b6662dd1e103a1" + integrity sha512-/5OFZgfIPSwy+YuIBP/FgJnQnsxhZhjjrnxudMddeblOouIodEQ75X14Rr4wGSG/bknL+Omy9iWlLo1u/9GzAA== dependencies: "@types/react" "*" -"@types/react-dom@17.0.2": - version "17.0.2" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.2.tgz#35654cf6c49ae162d5bc90843d5437dc38008d43" - integrity sha512-Icd9KEgdnFfJs39KyRyr0jQ7EKhq8U6CcHRMGAS45fp5qgUvxL3ujUCfWFttUK2UErqZNj97t9gsVPNAqcwoCg== - dependencies: - "@types/react" "*" - -"@types/react-redux@^7.1.20": - version "7.1.24" - resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.24.tgz#6caaff1603aba17b27d20f8ad073e4c077e975c0" - integrity sha512-7FkurKcS1k0FHZEtdbbgN8Oc6b+stGSfZYjQGicofJ0j4U0qIn/jaSvnP2pLwZKiai3/17xqqxkkrxTgN8UNbQ== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - "@types/react-router-config@*": version "5.0.6" resolved "https://registry.yarnpkg.com/@types/react-router-config/-/react-router-config-5.0.6.tgz#87c5c57e72d241db900d9734512c50ccec062451" @@ -3571,7 +3574,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*", "@types/react@17.0.43", "@types/react@^17.0.2": +"@types/react@*", "@types/react@^17.0.2": version "17.0.43" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.43.tgz#4adc142887dd4a2601ce730bc56c3436fdb07a55" integrity sha512-8Q+LNpdxf057brvPu1lMtC5Vn7J119xrP1aq4qiaefNioQUYANF/CYeK4NsKorSZyUGJ66g0IM+4bbjwx45o2A== @@ -3580,6 +3583,15 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@18.0.21": + version "18.0.21" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.21.tgz#b8209e9626bb00a34c76f55482697edd2b43cc67" + integrity sha512-7QUCOxvFgnD5Jk8ZKlUAhVcRj7GuJRjnjjiY/IUBWKgOlnvDvTMLD4RTF7NPyVmbRhNrbomZiOepg7M/2Kj1mA== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/retry@0.12.0": version "0.12.0" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" @@ -3629,6 +3641,11 @@ resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@types/ws@^8.5.1": version "8.5.3" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" @@ -5141,7 +5158,7 @@ crypto-random-string@^2.0.0: resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== -css-box-model@^1.2.0: +css-box-model@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -8595,10 +8612,10 @@ memfs@^3.1.2, memfs@^3.4.3: dependencies: fs-monkey "1.0.3" -memoize-one@^5.1.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" - integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== +memoize-one@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" + integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== merge-descriptors@1.0.1: version "1.0.1" @@ -10096,7 +10113,7 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== -raf-schd@^4.0.2: +raf-schd@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -10148,19 +10165,6 @@ react-base16-styling@^0.6.0: lodash.flow "^3.3.0" pure-color "^1.2.0" -react-beautiful-dnd@13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" - integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== - dependencies: - "@babel/runtime" "^7.9.2" - css-box-model "^1.2.0" - memoize-one "^5.1.1" - raf-schd "^4.0.2" - react-redux "^7.2.0" - redux "^4.0.4" - use-memo-one "^1.1.1" - react-confetti@6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.0.1.tgz#d4f57b5a021dd908a6243b8f63b6009b00818d10" @@ -10276,10 +10280,10 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== react-json-view@^1.21.3: version "1.21.3" @@ -10317,17 +10321,17 @@ react-query@3.39.0: broadcast-channel "^3.4.1" match-sorter "^6.0.2" -react-redux@^7.2.0: - version "7.2.8" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" - integrity sha512-6+uDjhs3PSIclqoCk0kd6iX74gzrGc3W5zcAjbrFgEdIjRSQObdIwfx80unTkVUYvbQ95Y8Av3OvFHq1w5EOUw== +react-redux@^8.0.2: + version "8.0.4" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.4.tgz#80c31dffa8af9526967c4267022ae1525ff0e36a" + integrity sha512-yMfQ7mX6bWuicz2fids6cR1YT59VTuT8MKyyE310wJQlINKENCeT1UcPdEiX6znI5tF8zXyJ/VYvDgeGuaaNwQ== dependencies: - "@babel/runtime" "^7.15.4" - "@types/react-redux" "^7.1.20" + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" hoist-non-react-statics "^3.3.2" - loose-envify "^1.4.0" - prop-types "^15.7.2" - react-is "^17.0.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" react-router-config@^5.1.1: version "5.1.1" @@ -10487,7 +10491,7 @@ recursive-readdir@^2.2.2: dependencies: minimatch "3.0.4" -redux@^4.0.0, redux@^4.0.4: +redux@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== @@ -12062,12 +12066,12 @@ use-latest@^1.2.1: dependencies: use-isomorphic-layout-effect "^1.1.1" -use-memo-one@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.2.tgz#0c8203a329f76e040047a35a1197defe342fab20" - integrity sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ== +use-memo-one@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== -use-sync-external-store@1.2.0: +use-sync-external-store@1.2.0, use-sync-external-store@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== From 83d9a1f3e22c8caa07902f5ba378f6930bf139a2 Mon Sep 17 00:00:00 2001 From: FRC Date: Wed, 5 Oct 2022 11:37:23 +0100 Subject: [PATCH 019/156] Posts changes (#988) * Add post subtitle * Add "Post" badge to post card * Move post tab to overview tab, refactor components * Fix styling nits. --- common/post.ts | 2 + functions/src/create-post.ts | 14 ++-- web/components/create-post.tsx | 23 +++++- web/components/groups/group-overview-post.tsx | 1 + web/components/groups/group-overview.tsx | 51 ++++++++++++- web/components/pinned-select-modal.tsx | 2 +- web/components/post-card.tsx | 36 ++++++++- web/pages/date-docs/create.tsx | 2 + web/pages/group/[...slugs]/index.tsx | 75 ------------------- web/pages/post/[...slugs]/index.tsx | 7 +- 10 files changed, 126 insertions(+), 87 deletions(-) diff --git a/common/post.ts b/common/post.ts index 45503b22..77130a2c 100644 --- a/common/post.ts +++ b/common/post.ts @@ -3,6 +3,7 @@ import { JSONContent } from '@tiptap/core' export type Post = { id: string title: string + subtitle: string content: JSONContent creatorId: string // User id createdTime: number @@ -17,3 +18,4 @@ export type DateDoc = Post & { } export const MAX_POST_TITLE_LENGTH = 480 +export const MAX_POST_SUBTITLE_LENGTH = 480 diff --git a/functions/src/create-post.ts b/functions/src/create-post.ts index e9d6ae8f..96e3c66a 100644 --- a/functions/src/create-post.ts +++ b/functions/src/create-post.ts @@ -3,7 +3,11 @@ 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 } from '../../common/post' +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' @@ -36,6 +40,7 @@ const contentSchema: z.ZodType = z.lazy(() => 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(), @@ -48,10 +53,8 @@ const postSchema = z.object({ export const createpost = newEndpoint({}, async (req, auth) => { const firestore = admin.firestore() - const { title, content, groupId, question, ...otherProps } = validate( - postSchema, - req.body - ) + const { title, subtitle, content, groupId, question, ...otherProps } = + validate(postSchema, req.body) const creator = await getUser(auth.uid) if (!creator) @@ -89,6 +92,7 @@ export const createpost = newEndpoint({}, async (req, auth) => { creatorId: creator.id, slug, title, + subtitle, createdTime: Date.now(), content: content, contractSlug, diff --git a/web/components/create-post.tsx b/web/components/create-post.tsx index c176e61d..6d42051c 100644 --- a/web/components/create-post.tsx +++ b/web/components/create-post.tsx @@ -13,6 +13,8 @@ import { Group } from 'common/group' export function CreatePost(props: { group?: Group }) { const [title, setTitle] = useState('') + const [subtitle, setSubtitle] = useState('') + const [error, setError] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) @@ -22,12 +24,17 @@ export function CreatePost(props: { group?: Group }) { disabled: isSubmitting, }) - const isValid = editor && title.length > 0 && editor.isEmpty === false + const isValid = + editor && + title.length > 0 && + subtitle.length > 0 && + editor.isEmpty === false async function savePost(title: string) { if (!editor) return const newPost = { title: title, + subtitle: subtitle, content: editor.getJSON(), groupId: group?.id, } @@ -62,6 +69,20 @@ export function CreatePost(props: { group?: Group }) { onChange={(e) => setTitle(e.target.value || '')} /> + +