From 00ba3b0c4870f515a0df643c9dbb701c2c912930 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Thu, 1 Sep 2022 16:23:12 -0600 Subject: [PATCH] Show avatars of tippers and unique bettors (#837) * Show avatars of tippers and unique bettors * Make transparent the avatar bg * fix import --- common/notification.ts | 1 + functions/src/create-notification.ts | 54 +++++++++----- functions/src/on-create-bet.ts | 31 ++++---- web/components/button.tsx | 4 +- .../multi-user-transaction-link.tsx | 74 +++++++++++++++++++ web/components/user-link.tsx | 70 +----------------- web/pages/notifications.tsx | 74 +++++++++---------- 7 files changed, 162 insertions(+), 146 deletions(-) create mode 100644 web/components/multi-user-transaction-link.tsx diff --git a/common/notification.ts b/common/notification.ts index 657ea2c1..9ec320fa 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -15,6 +15,7 @@ export type Notification = { sourceUserUsername?: string sourceUserAvatarUrl?: string sourceText?: string + data?: string sourceContractTitle?: string sourceContractCreatorUsername?: string diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 8ed14704..131d6e85 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -151,15 +151,6 @@ export const createNotification = async ( } } - const notifyContractCreatorOfUniqueBettorsBonus = async ( - userToReasonTexts: user_to_reason_texts, - userId: string - ) => { - userToReasonTexts[userId] = { - reason: 'unique_bettors_on_your_contract', - } - } - const userToReasonTexts: user_to_reason_texts = {} // The following functions modify the userToReasonTexts object in place. @@ -192,16 +183,6 @@ export const createNotification = async ( sourceContract ) { await notifyContractCreator(userToReasonTexts, sourceContract) - } else if ( - sourceType === 'bonus' && - sourceUpdateType === 'created' && - sourceContract - ) { - // Note: the daily bonus won't have a contract attached to it - await notifyContractCreatorOfUniqueBettorsBonus( - userToReasonTexts, - sourceContract.creatorId - ) } await createUsersNotifications(userToReasonTexts) @@ -737,3 +718,38 @@ export async function filterUserIdsForOnlyFollowerIds( ) return userIds.filter((id) => contractFollowersIds.includes(id)) } + +export const createUniqueBettorBonusNotification = async ( + contractCreatorId: string, + bettor: User, + txnId: string, + contract: Contract, + amount: number, + idempotencyKey: string +) => { + 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, + } + return await notificationRef.set(removeUndefinedProps(notification)) +} diff --git a/functions/src/on-create-bet.ts b/functions/src/on-create-bet.ts index ff6cf9d9..5dbebfc3 100644 --- a/functions/src/on-create-bet.ts +++ b/functions/src/on-create-bet.ts @@ -7,7 +7,7 @@ import { getUser, getValues, isProd, log } from './utils' import { createBetFillNotification, createBettingStreakBonusNotification, - createNotification, + createUniqueBettorBonusNotification, } from './create-notification' import { filterDefined } from '../../common/util/array' import { Contract } from '../../common/contract' @@ -54,11 +54,11 @@ export const onCreateBet = functions.firestore log(`Could not find contract ${contractId}`) return } - await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bet.userId) const bettor = await getUser(bet.userId) if (!bettor) return + await updateUniqueBettorsAndGiveCreatorBonus(contract, eventId, bettor) await notifyFills(bet, contract, eventId, bettor) await updateBettingStreak(bettor, bet, contract, eventId) @@ -126,7 +126,7 @@ const updateBettingStreak = async ( const updateUniqueBettorsAndGiveCreatorBonus = async ( contract: Contract, eventId: string, - bettorId: string + bettor: User ) => { let previousUniqueBettorIds = contract.uniqueBettorIds @@ -147,13 +147,13 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( ) } - const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettorId) + const isNewUniqueBettor = !previousUniqueBettorIds.includes(bettor.id) - const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettorId]) + const newUniqueBettorIds = uniq([...previousUniqueBettorIds, bettor.id]) // Update contract unique bettors if (!contract.uniqueBettorIds || isNewUniqueBettor) { log(`Got ${previousUniqueBettorIds} unique bettors`) - isNewUniqueBettor && log(`And a new unique bettor ${bettorId}`) + isNewUniqueBettor && log(`And a new unique bettor ${bettor.id}`) await firestore.collection(`contracts`).doc(contract.id).update({ uniqueBettorIds: newUniqueBettorIds, uniqueBettorCount: newUniqueBettorIds.length, @@ -161,7 +161,7 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( } // No need to give a bonus for the creator's bet - if (!isNewUniqueBettor || bettorId == contract.creatorId) return + if (!isNewUniqueBettor || bettor.id == contract.creatorId) return // Create combined txn for all new unique bettors const bonusTxnDetails = { @@ -192,18 +192,13 @@ const updateUniqueBettorsAndGiveCreatorBonus = async ( log(`No bonus for user: ${contract.creatorId} - reason:`, result.status) } else { log(`Bonus txn for user: ${contract.creatorId} completed:`, result.txn?.id) - await createNotification( + await createUniqueBettorBonusNotification( + contract.creatorId, + bettor, result.txn.id, - 'bonus', - 'created', - fromUser, - eventId + '-bonus', - result.txn.amount + '', - { - contract, - slug: contract.slug, - title: contract.question, - } + contract, + result.txn.amount, + eventId + '-unique-bettor-bonus' ) } } diff --git a/web/components/button.tsx b/web/components/button.tsx index dbb28122..cb39cba8 100644 --- a/web/components/button.tsx +++ b/web/components/button.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react' +import { MouseEventHandler, ReactNode } from 'react' import clsx from 'clsx' export type SizeType = '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' @@ -14,7 +14,7 @@ export type ColorType = export function Button(props: { className?: string - onClick?: () => void + onClick?: MouseEventHandler | undefined children?: ReactNode size?: SizeType color?: ColorType diff --git a/web/components/multi-user-transaction-link.tsx b/web/components/multi-user-transaction-link.tsx new file mode 100644 index 00000000..70d273db --- /dev/null +++ b/web/components/multi-user-transaction-link.tsx @@ -0,0 +1,74 @@ +import { useState } from 'react' +import { Row } from 'web/components/layout/row' +import { Modal } from 'web/components/layout/modal' +import { Col } from 'web/components/layout/col' +import { formatMoney } from 'common/util/format' +import { Avatar } from 'web/components/avatar' +import { UserLink } from 'web/components/user-link' +import { Button } from 'web/components/button' + +export type MultiUserLinkInfo = { + name: string + username: string + avatarUrl: string | undefined + amount: number +} + +export function MultiUserTransactionLink(props: { + userInfos: MultiUserLinkInfo[] + modalLabel: string +}) { + const { userInfos, modalLabel } = props + const [open, setOpen] = useState(false) + const maxShowCount = 5 + return ( + + + + + {modalLabel} + {userInfos.map((userInfo) => ( + + + +{formatMoney(userInfo.amount)} + + + + + ))} + + + + ) +} diff --git a/web/components/user-link.tsx b/web/components/user-link.tsx index cc8f1a1f..e1b675a0 100644 --- a/web/components/user-link.tsx +++ b/web/components/user-link.tsx @@ -1,13 +1,7 @@ -import { linkClass, SiteLink } from 'web/components/site-link' +import { SiteLink } from 'web/components/site-link' import clsx from 'clsx' -import { Row } from 'web/components/layout/row' -import { Modal } from 'web/components/layout/modal' -import { Col } from 'web/components/layout/col' -import { useState } from 'react' -import { Avatar } from 'web/components/avatar' -import { formatMoney } from 'common/util/format' -function shortenName(name: string) { +export function shortenName(name: string) { const firstName = name.split(' ')[0] const maxLength = 11 const shortName = @@ -38,63 +32,3 @@ export function UserLink(props: { ) } - -export type MultiUserLinkInfo = { - name: string - username: string - avatarUrl: string | undefined - amountTipped: number -} - -export function MultiUserTipLink(props: { - userInfos: MultiUserLinkInfo[] - className?: string -}) { - const { userInfos, className } = props - const [open, setOpen] = useState(false) - const maxShowCount = 2 - return ( - <> - { - e.stopPropagation() - setOpen(true) - }} - > - {userInfos.map((userInfo, index) => - index < maxShowCount ? ( - - {shortenName(userInfo.name) + - (index < maxShowCount - 1 ? ', ' : '')} - - ) : ( - - & {userInfos.length - maxShowCount} more - - ) - )} - - - - Who tipped you - {userInfos.map((userInfo) => ( - - - +{formatMoney(userInfo.amountTipped)} - - - - - ))} - - - - ) -} diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 2b2e8d7a..2ec3ac6f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -43,12 +43,13 @@ import { SiteLink } from 'web/components/site-link' import { NotificationSettings } from 'web/components/NotificationSettings' import { SEO } from 'web/components/SEO' import { usePrivateUser, useUser } from 'web/hooks/use-user' -import { - MultiUserTipLink, - MultiUserLinkInfo, - UserLink, -} from 'web/components/user-link' +import { UserLink } from 'web/components/user-link' import { LoadingIndicator } from 'web/components/loading-indicator' +import { + MultiUserLinkInfo, + MultiUserTransactionLink, +} from 'web/components/multi-user-transaction-link' +import { Col } from 'web/components/layout/col' export const NOTIFICATIONS_PER_PAGE = 30 const HIGHLIGHT_CLASS = 'bg-indigo-50' @@ -212,7 +213,7 @@ function IncomeNotificationGroupItem(props: { function combineNotificationsByAddingNumericSourceTexts( notifications: Notification[] ) { - const newNotifications = [] + const newNotifications: Notification[] = [] const groupedNotificationsBySourceType = groupBy( notifications, (n) => n.sourceType @@ -228,10 +229,7 @@ function IncomeNotificationGroupItem(props: { for (const sourceTitle in groupedNotificationsBySourceTitle) { const notificationsForSourceTitle = groupedNotificationsBySourceTitle[sourceTitle] - if (notificationsForSourceTitle.length === 1) { - newNotifications.push(notificationsForSourceTitle[0]) - continue - } + let sum = 0 notificationsForSourceTitle.forEach( (notification) => @@ -251,7 +249,7 @@ function IncomeNotificationGroupItem(props: { username: notification.sourceUserUsername, name: notification.sourceUserName, avatarUrl: notification.sourceUserAvatarUrl, - amountTipped: thisSum, + amount: thisSum, } as MultiUserLinkInfo }), (n) => n.username @@ -260,10 +258,8 @@ function IncomeNotificationGroupItem(props: { const newNotification = { ...notificationsForSourceTitle[0], sourceText: sum.toString(), - sourceUserUsername: - uniqueUsers.length > 1 - ? JSON.stringify(uniqueUsers) - : notificationsForSourceTitle[0].sourceType, + sourceUserUsername: notificationsForSourceTitle[0].sourceUserUsername, + data: JSON.stringify(uniqueUsers), } newNotifications.push(newNotification) } @@ -372,12 +368,15 @@ function IncomeNotificationItem(props: { justSummary?: boolean }) { const { notification, justSummary } = props - const { sourceType, sourceUserName, sourceUserUsername, sourceText } = - notification + const { sourceType, sourceUserUsername, sourceText, data } = notification const [highlighted] = useState(!notification.isSeen) const { width } = useWindowSize() const isMobile = (width && width < 768) || false const user = useUser() + const isTip = sourceType === 'tip' || sourceType === 'tip_and_like' + const isUniqueBettorBonus = sourceType === 'bonus' + const userLinks: MultiUserLinkInfo[] = + isTip || isUniqueBettorBonus ? JSON.parse(data ?? '{}') : [] useEffect(() => { setNotificationsAsSeen([notification]) @@ -505,29 +504,26 @@ function IncomeNotificationItem(props: { href={getIncomeSourceUrl() ?? ''} className={'absolute left-0 right-0 top-0 bottom-0 z-0'} /> - -
-
- {incomeNotificationLabel()} -
- - {(sourceType === 'tip' || sourceType === 'tip_and_like') && - (sourceUserUsername?.includes(',') ? ( - - ) : ( - - ))} - {reasonAndLink(false)} + + {(isTip || isUniqueBettorBonus) && ( + + )} + + {incomeNotificationLabel()} + + {isTip && + (userLinks.length > 1 + ? 'Multiple users' + : userLinks.length > 0 + ? userLinks[0].name + : '')} -
-
+ {reasonAndLink(false)} + +