From aad5f6528bb68c68ff6d24e43026d11b501450ba Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Tue, 30 Aug 2022 17:13:25 -0600 Subject: [PATCH] new market view (#819) * Show old details on lg, don't unfill heart * Hide tip market if creator * Small ui tweaks * Remove contract. calls * Update high-medium-low * Remove unused bets prop * Show uniques * Remove unused bets prop --- common/like.ts | 2 +- functions/src/index.ts | 3 +- functions/src/on-delete-like.ts | 32 ---- .../{on-create-like.ts => on-update-like.ts} | 42 ++++- web/components/contract/contract-details.tsx | 145 +++++++++++++----- .../contract/contract-info-dialog.tsx | 21 ++- web/components/contract/contract-overview.tsx | 43 +++--- ...row.tsx => extra-contract-actions-row.tsx} | 51 +++--- .../contract/like-market-button.tsx | 24 ++- web/components/contract/share-modal.tsx | 38 ++++- web/components/follow-market-button.tsx | 10 +- web/components/user-link.tsx | 2 +- web/pages/embed/[username]/[contractSlug].tsx | 8 +- 13 files changed, 245 insertions(+), 176 deletions(-) delete mode 100644 functions/src/on-delete-like.ts rename functions/src/{on-create-like.ts => on-update-like.ts} (61%) rename web/components/contract/{share-row.tsx => extra-contract-actions-row.tsx} (51%) diff --git a/common/like.ts b/common/like.ts index 85140e02..38b25dad 100644 --- a/common/like.ts +++ b/common/like.ts @@ -3,6 +3,6 @@ export type Like = { userId: string type: 'contract' createdTime: number - tipTxnId?: string + tipTxnId?: string // only holds most recent tip txn id } export const LIKE_TIP_AMOUNT = 5 diff --git a/functions/src/index.ts b/functions/src/index.ts index 6ede39a0..2ec7f3ce 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -31,8 +31,7 @@ export * from './weekly-markets-emails' export * from './reset-betting-streaks' export * from './reset-weekly-emails-flag' export * from './on-update-contract-follow' -export * from './on-create-like' -export * from './on-delete-like' +export * from './on-update-like' // v2 export * from './health' diff --git a/functions/src/on-delete-like.ts b/functions/src/on-delete-like.ts deleted file mode 100644 index 151614b0..00000000 --- a/functions/src/on-delete-like.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as functions from 'firebase-functions' -import * as admin from 'firebase-admin' -import { Like } from '../../common/like' -import { getContract, log } from './utils' -import { uniq } from 'lodash' - -const firestore = admin.firestore() - -export const onDeleteLike = functions.firestore - .document('users/{userId}/likes/{likeId}') - .onDelete(async (change) => { - const like = change.data() as Like - if (like.type === 'contract') { - await removeContractLike(like) - } - }) - -const removeContractLike = async (like: Like) => { - const contract = await getContract(like.id) - if (!contract) { - log('Could not find contract') - return - } - const likedByUserIds = uniq(contract.likedByUserIds ?? []) - const newLikedByUserIds = likedByUserIds.filter( - (userId) => userId !== like.userId - ) - await firestore.collection('contracts').doc(like.id).update({ - likedByUserIds: newLikedByUserIds, - likedByUserCount: newLikedByUserIds.length, - }) -} diff --git a/functions/src/on-create-like.ts b/functions/src/on-update-like.ts similarity index 61% rename from functions/src/on-create-like.ts rename to functions/src/on-update-like.ts index 8c5885b0..7633c395 100644 --- a/functions/src/on-create-like.ts +++ b/functions/src/on-update-like.ts @@ -19,14 +19,36 @@ export const onCreateLike = functions.firestore } }) +export const onUpdateLike = functions.firestore + .document('users/{userId}/likes/{likeId}') + .onUpdate(async (change, context) => { + const like = change.after.data() as Like + const prevLike = change.before.data() as Like + const { eventId } = context + if (like.type === 'contract' && like.tipTxnId !== prevLike.tipTxnId) { + await handleCreateLikeNotification(like, eventId) + await updateContractLikes(like) + } + }) + +export const onDeleteLike = functions.firestore + .document('users/{userId}/likes/{likeId}') + .onDelete(async (change) => { + const like = change.data() as Like + if (like.type === 'contract') { + await removeContractLike(like) + } + }) + const updateContractLikes = async (like: Like) => { const contract = await getContract(like.id) if (!contract) { log('Could not find contract') return } - const likedByUserIds = uniq(contract.likedByUserIds ?? []) - likedByUserIds.push(like.userId) + const likedByUserIds = uniq( + (contract.likedByUserIds ?? []).concat(like.userId) + ) await firestore .collection('contracts') .doc(like.id) @@ -69,3 +91,19 @@ const handleCreateLikeNotification = async (like: Like, eventId: string) => { tipTxnData ) } + +const removeContractLike = async (like: Like) => { + const contract = await getContract(like.id) + if (!contract) { + log('Could not find contract') + return + } + const likedByUserIds = uniq(contract.likedByUserIds ?? []) + const newLikedByUserIds = likedByUserIds.filter( + (userId) => userId !== like.userId + ) + await firestore.collection('contracts').doc(like.id).update({ + likedByUserIds: newLikedByUserIds, + likedByUserCount: newLikedByUserIds.length, + }) +} diff --git a/web/components/contract/contract-details.tsx b/web/components/contract/contract-details.tsx index 72ecbb1f..2e76531b 100644 --- a/web/components/contract/contract-details.tsx +++ b/web/components/contract/contract-details.tsx @@ -18,7 +18,6 @@ import { fromNow } from 'web/lib/util/time' import { Avatar } from '../avatar' import { useState } from 'react' import { ContractInfoDialog } from './contract-info-dialog' -import { Bet } from 'common/bet' import NewContractBadge from '../new-contract-badge' import { UserFollowButton } from '../follow-button' import { DAY_MS } from 'common/util/time' @@ -35,6 +34,8 @@ import { contractMetrics } from 'common/contract-details' import { User } from 'common/user' import { UserLink } from 'web/components/user-link' import { FeaturedContractBadge } from 'web/components/contract/featured-contract-badge' +import { Tooltip } from 'web/components/tooltip' +import { useWindowSize } from 'web/hooks/use-window-size' export type ShowTime = 'resolve-date' | 'close-date' @@ -78,7 +79,7 @@ export function MiscDetails(props: { ) : (contract?.featuredOnHomeRank ?? 0) > 0 ? ( ) : volume > 0 || !isNew ? ( - {formatMoney(contract.volume)} bet + {formatMoney(volume)} bet ) : ( )} @@ -101,7 +102,7 @@ export function AvatarDetails(props: { short?: boolean }) { const { contract, short, className } = props - const { creatorName, creatorUsername } = contract + const { creatorName, creatorUsername, creatorAvatarUrl } = contract return ( @@ -138,20 +139,28 @@ export function AbbrContractDetails(props: { export function ContractDetails(props: { contract: Contract - bets: Bet[] user: User | null | undefined isCreator?: boolean disabled?: boolean }) { - const { contract, bets, isCreator, disabled } = props - const { closeTime, creatorName, creatorUsername, creatorId, groupLinks } = - contract + const { contract, isCreator, disabled } = props + const { + closeTime, + creatorName, + creatorUsername, + creatorId, + groupLinks, + creatorAvatarUrl, + resolutionTime, + } = contract const { volumeLabel, resolvedDate } = contractMetrics(contract) const groupToDisplay = groupLinks?.sort((a, b) => a.createdTime - b.createdTime)[0] ?? null const user = useUser() const [open, setOpen] = useState(false) + const { width } = useWindowSize() + const isMobile = (width ?? 0) < 600 const groupInfo = ( @@ -167,7 +176,7 @@ export function ContractDetails(props: { @@ -178,6 +187,7 @@ export function ContractDetails(props: { className="whitespace-nowrap" name={creatorName} username={creatorUsername} + short={isMobile} /> )} {!disabled && } @@ -228,14 +238,11 @@ export function ContractDetails(props: { {(!!closeTime || !!resolvedDate) && ( - - {resolvedDate && contract.resolutionTime ? ( + + {resolvedDate && resolutionTime ? ( <> - + {resolvedDate} @@ -255,17 +262,84 @@ export function ContractDetails(props: { )} {user && ( <> - +
{volumeLabel}
- {!disabled && } + {!disabled && ( + + )} )}
) } +export function ExtraMobileContractDetails(props: { + contract: Contract + user: User | null | undefined + forceShowVolume?: boolean +}) { + const { contract, user, forceShowVolume } = props + const { volume, resolutionTime, closeTime, creatorId, uniqueBettorCount } = + contract + const uniqueBettors = uniqueBettorCount ?? 0 + const { resolvedDate } = contractMetrics(contract) + const volumeTranslation = + volume > 800 || uniqueBettors > 20 + ? 'High' + : volume > 300 || uniqueBettors > 10 + ? 'Medium' + : 'Low' + + return ( + + {resolvedDate && resolutionTime ? ( + + + + {resolvedDate} + + + Ended + + ) : ( + !resolvedDate && + closeTime && ( + + + Ends + + ) + )} + {(user || forceShowVolume) && ( + + + {volumeTranslation} + + Activity + + )} + + ) +} + function EditableCloseDate(props: { closeTime: number contract: Contract @@ -318,10 +392,10 @@ function EditableCloseDate(props: { return ( <> {isEditingCloseTime ? ( - + e.stopPropagation()} onChange={(e) => setCloseDate(e.target.value)} min={Date.now()} @@ -329,39 +403,32 @@ function EditableCloseDate(props: { /> e.stopPropagation()} onChange={(e) => setCloseHoursMinutes(e.target.value)} min="00:00" value={closeHoursMinutes} /> + ) : ( Date.now() ? 'Trading ends:' : 'Trading ended:'} time={closeTime} > - {isSameYear - ? dayJsCloseTime.format('MMM D') - : dayJsCloseTime.format('MMM D, YYYY')} - {isSameDay && <> ({fromNow(closeTime)})} + isCreator && setIsEditingCloseTime(true)} + > + {isSameYear + ? dayJsCloseTime.format('MMM D') + : dayJsCloseTime.format('MMM D, YYYY')} + {isSameDay && <> ({fromNow(closeTime)})} + )} - - {isCreator && - (isEditingCloseTime ? ( - - ) : ( - - ))} ) } diff --git a/web/components/contract/contract-info-dialog.tsx b/web/components/contract/contract-info-dialog.tsx index f418db06..aaa3cad6 100644 --- a/web/components/contract/contract-info-dialog.tsx +++ b/web/components/contract/contract-info-dialog.tsx @@ -1,9 +1,7 @@ import { DotsHorizontalIcon } from '@heroicons/react/outline' import clsx from 'clsx' import dayjs from 'dayjs' -import { uniqBy } from 'lodash' import { useState } from 'react' -import { Bet } from 'common/bet' import { Contract } from 'common/contract' import { formatMoney } from 'common/util/format' @@ -22,8 +20,11 @@ import ShortToggle from '../widgets/short-toggle' export const contractDetailsButtonClassName = 'group flex items-center rounded-md px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-100 text-gray-400 hover:text-gray-500' -export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { - const { contract, bets } = props +export function ContractInfoDialog(props: { + contract: Contract + className?: string +}) { + const { contract, className } = props const [open, setOpen] = useState(false) const [featured, setFeatured] = useState( @@ -37,11 +38,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { const { createdTime, closeTime, resolutionTime, mechanism, outcomeType, id } = contract - const tradersCount = uniqBy( - bets.filter((bet) => !bet.isAnte), - 'userId' - ).length - + const bettorsCount = contract.uniqueBettorCount ?? 'Unknown' const typeDisplay = outcomeType === 'BINARY' ? 'YES / NO' @@ -69,7 +66,7 @@ export function ContractInfoDialog(props: { contract: Contract; bets: Bet[] }) { return ( <> - {showChallenge && ( - - )} -
+ {user?.id !== contract.creatorId && ( -
+ )} + + +
) } diff --git a/web/components/contract/like-market-button.tsx b/web/components/contract/like-market-button.tsx index f4fed287..0fed0518 100644 --- a/web/components/contract/like-market-button.tsx +++ b/web/components/contract/like-market-button.tsx @@ -6,10 +6,11 @@ import { User } from 'common/user' import { useUserLikes } from 'web/hooks/use-likes' import toast from 'react-hot-toast' import { formatMoney } from 'common/util/format' -import { likeContract, unLikeContract } from 'web/lib/firebase/likes' +import { likeContract } from 'web/lib/firebase/likes' import { LIKE_TIP_AMOUNT } from 'common/like' import clsx from 'clsx' -import { Row } from 'web/components/layout/row' +import { Col } from 'web/components/layout/col' +import { firebaseLogin } from 'web/lib/firebase/users' export function LikeMarketButton(props: { contract: Contract @@ -18,16 +19,12 @@ export function LikeMarketButton(props: { const { contract, user } = props const likes = useUserLikes(user?.id) - const likedContractIds = likes + const userLikedContractIds = likes ?.filter((l) => l.type === 'contract') .map((l) => l.id) - if (!user) return
const onLike = async () => { - if (likedContractIds?.includes(contract.id)) { - await unLikeContract(user.id, contract.id) - return - } + if (!user) return firebaseLogin() await likeContract(user, contract) toast(`You tipped ${contract.creatorName} ${formatMoney(LIKE_TIP_AMOUNT)}!`) } @@ -39,18 +36,19 @@ export function LikeMarketButton(props: { color={'gray-white'} onClick={onLike} > - + - Tip - + Tip + ) } diff --git a/web/components/contract/share-modal.tsx b/web/components/contract/share-modal.tsx index 2c74a5a4..5bae101d 100644 --- a/web/components/contract/share-modal.tsx +++ b/web/components/contract/share-modal.tsx @@ -12,12 +12,15 @@ import { TweetButton } from '../tweet-button' import { DuplicateContractButton } from '../copy-contract-button' import { Button } from '../button' import { copyToClipboard } from 'web/lib/util/copy' -import { track } from 'web/lib/service/analytics' +import { track, withTracking } from 'web/lib/service/analytics' import { ENV_CONFIG } from 'common/envs/constants' import { User } from 'common/user' import { SiteLink } from '../site-link' import { formatMoney } from 'common/util/format' import { REFERRAL_AMOUNT } from 'common/economy' +import { CreateChallengeModal } from 'web/components/challenges/create-challenge-modal' +import { useState } from 'react' +import { CHALLENGES_ENABLED } from 'common/challenge' export function ShareModal(props: { contract: Contract @@ -26,8 +29,13 @@ export function ShareModal(props: { setOpen: (open: boolean) => void }) { const { contract, user, isOpen, setOpen } = props + const { outcomeType, resolution } = contract + const [openCreateChallengeModal, setOpenCreateChallengeModal] = + useState(false) const linkIcon =