Show avatars of tippers and unique bettors (#837)

* Show avatars of tippers and unique bettors

* Make transparent the avatar bg

* fix import
This commit is contained in:
Ian Philips 2022-09-01 16:23:12 -06:00 committed by GitHub
parent 7508d86c73
commit 00ba3b0c48
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 162 additions and 146 deletions

View File

@ -15,6 +15,7 @@ export type Notification = {
sourceUserUsername?: string
sourceUserAvatarUrl?: string
sourceText?: string
data?: string
sourceContractTitle?: string
sourceContractCreatorUsername?: string

View File

@ -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))
}

View File

@ -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'
)
}
}

View File

@ -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<any> | undefined
children?: ReactNode
size?: SizeType
color?: ColorType

View File

@ -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 (
<Row>
<Button
size={'xs'}
color={'gray-white'}
className={'z-10 mr-1 gap-1 bg-transparent'}
onClick={(e) => {
e.stopPropagation()
setOpen(true)
}}
>
<Row className={'gap-1'}>
{userInfos.map((userInfo, index) =>
index < maxShowCount ? (
<Row key={userInfo.username + 'shortened'}>
<Avatar
username={userInfo.username}
size={'sm'}
avatarUrl={userInfo.avatarUrl}
noLink={userInfos.length > 1}
/>
</Row>
) : (
<span>& {userInfos.length - maxShowCount} more</span>
)
)}
</Row>
</Button>
<Modal open={open} setOpen={setOpen} size={'sm'}>
<Col className="items-start gap-4 rounded-md bg-white p-6">
<span className={'text-xl'}>{modalLabel}</span>
{userInfos.map((userInfo) => (
<Row
key={userInfo.username + 'list'}
className="w-full items-center gap-2"
>
<span className="text-primary min-w-[3.5rem]">
+{formatMoney(userInfo.amount)}
</span>
<Avatar
username={userInfo.username}
avatarUrl={userInfo.avatarUrl}
/>
<UserLink name={userInfo.name} username={userInfo.username} />
</Row>
))}
</Col>
</Modal>
</Row>
)
}

View File

@ -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: {
</SiteLink>
)
}
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 (
<>
<Row
className={clsx('mr-1 inline-flex gap-1', linkClass, className)}
onClick={(e) => {
e.stopPropagation()
setOpen(true)
}}
>
{userInfos.map((userInfo, index) =>
index < maxShowCount ? (
<span key={userInfo.username + 'shortened'} className={linkClass}>
{shortenName(userInfo.name) +
(index < maxShowCount - 1 ? ', ' : '')}
</span>
) : (
<span className={linkClass}>
& {userInfos.length - maxShowCount} more
</span>
)
)}
</Row>
<Modal open={open} setOpen={setOpen} size={'sm'}>
<Col className="items-start gap-4 rounded-md bg-white p-6">
<span className={'text-xl'}>Who tipped you</span>
{userInfos.map((userInfo) => (
<Row
key={userInfo.username + 'list'}
className="w-full items-center gap-2"
>
<span className="text-primary min-w-[3.5rem]">
+{formatMoney(userInfo.amountTipped)}
</span>
<Avatar
username={userInfo.username}
avatarUrl={userInfo.avatarUrl}
/>
<UserLink name={userInfo.name} username={userInfo.username} />
</Row>
))}
</Col>
</Modal>
</>
)
}

View File

@ -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'}
/>
<Row className={'items-center text-gray-500 sm:justify-start'}>
<div className={'line-clamp-2 flex max-w-xl shrink '}>
<div className={'inline'}>
<span className={'mr-1'}>{incomeNotificationLabel()}</span>
</div>
<span>
{(sourceType === 'tip' || sourceType === 'tip_and_like') &&
(sourceUserUsername?.includes(',') ? (
<MultiUserTipLink
userInfos={JSON.parse(sourceUserUsername)}
/>
) : (
<UserLink
name={sourceUserName || ''}
username={sourceUserUsername || ''}
className={'mr-1 flex-shrink-0'}
short={true}
/>
))}
{reasonAndLink(false)}
<Col className={'justify-start text-gray-500'}>
{(isTip || isUniqueBettorBonus) && (
<MultiUserTransactionLink
userInfos={userLinks}
modalLabel={isTip ? 'Who tipped you' : 'Unique bettors'}
/>
)}
<Row className={'line-clamp-2 flex max-w-xl'}>
<span>{incomeNotificationLabel()}</span>
<span className={'mx-1'}>
{isTip &&
(userLinks.length > 1
? 'Multiple users'
: userLinks.length > 0
? userLinks[0].name
: '')}
</span>
</div>
</Row>
<span>{reasonAndLink(false)}</span>
</Row>
</Col>
<div className={'border-b border-gray-300 pt-4'} />
</div>
</div>