Show tip notifications (#623)

* Show tip notifications

* Optimizing notifications for mobile

* Unused vars

* Move income reason logic to income notif

* Remove unnecessary icons

* Unused vars
This commit is contained in:
Ian Philips 2022-07-06 13:30:51 -06:00 committed by GitHub
parent 2d1e76eae8
commit de20ee9fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 391 additions and 227 deletions

View File

@ -15,6 +15,7 @@ import { ENV_CONFIG } from './envs/constants'
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100 export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id
export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id
export function getCpmmInitialLiquidity( export function getCpmmInitialLiquidity(
providerId: string, providerId: string,

View File

@ -61,3 +61,4 @@ export type notification_reason_types =
| 'user_joined_to_bet_on_your_market' | 'user_joined_to_bet_on_your_market'
| 'unique_bettors_on_your_contract' | 'unique_bettors_on_your_contract'
| 'on_group_you_are_member_of' | 'on_group_you_are_member_of'
| 'tip_received'

View File

@ -66,9 +66,7 @@ export const createNotification = async (
sourceUserAvatarUrl: sourceUser.avatarUrl, sourceUserAvatarUrl: sourceUser.avatarUrl,
sourceText, sourceText,
sourceContractCreatorUsername: sourceContract?.creatorUsername, sourceContractCreatorUsername: sourceContract?.creatorUsername,
// TODO: move away from sourceContractTitle to sourceTitle
sourceContractTitle: sourceContract?.question, sourceContractTitle: sourceContract?.question,
// TODO: move away from sourceContractSlug to sourceSlug
sourceContractSlug: sourceContract?.slug, sourceContractSlug: sourceContract?.slug,
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
@ -278,13 +276,22 @@ export const createNotification = async (
} }
const notifyOtherGroupMembersOfComment = async ( const notifyOtherGroupMembersOfComment = async (
userToReasons: user_to_reason_texts,
userId: string
) => {
if (shouldGetNotification(userId, userToReasons))
userToReasons[userId] = {
reason: 'on_group_you_are_member_of',
isSeeOnHref: sourceSlug,
}
}
const notifyTippedUserOfNewTip = async (
userToReasonTexts: user_to_reason_texts, userToReasonTexts: user_to_reason_texts,
userId: string userId: string
) => { ) => {
if (shouldGetNotification(userId, userToReasonTexts)) if (shouldGetNotification(userId, userToReasonTexts))
userToReasonTexts[userId] = { userToReasonTexts[userId] = {
reason: 'on_group_you_are_member_of', reason: 'tip_received',
isSeeOnHref: sourceSlug,
} }
} }
@ -304,6 +311,7 @@ export const createNotification = async (
// The following functions need sourceContract to be defined. // The following functions need sourceContract to be defined.
if (!sourceContract) return userToReasonTexts if (!sourceContract) return userToReasonTexts
if ( if (
sourceType === 'comment' || sourceType === 'comment' ||
sourceType === 'answer' || sourceType === 'answer' ||
@ -338,6 +346,8 @@ export const createNotification = async (
userToReasonTexts, userToReasonTexts,
sourceContract.creatorId sourceContract.creatorId
) )
} else if (sourceType === 'tip' && relatedUserId) {
await notifyTippedUserOfNewTip(userToReasonTexts, relatedUserId)
} }
return userToReasonTexts return userToReasonTexts
} }

View File

@ -1,11 +1,14 @@
import { APIError, newEndpoint } from './api' import { APIError, newEndpoint } from './api'
import { log } from './utils' import { isProd, log } from './utils'
import * as admin from 'firebase-admin' import * as admin from 'firebase-admin'
import { PrivateUser } from '../../common/lib/user' import { PrivateUser } from '../../common/lib/user'
import { uniq } from 'lodash' import { uniq } from 'lodash'
import { Bet } from '../../common/lib/bet' import { Bet } from '../../common/lib/bet'
const firestore = admin.firestore() const firestore = admin.firestore()
import { HOUSE_LIQUIDITY_PROVIDER_ID } from '../../common/antes' import {
DEV_HOUSE_LIQUIDITY_PROVIDER_ID,
HOUSE_LIQUIDITY_PROVIDER_ID,
} from '../../common/antes'
import { runTxn, TxnData } from './transact' import { runTxn, TxnData } from './transact'
import { createNotification } from './create-notification' import { createNotification } from './create-notification'
import { User } from '../../common/lib/user' import { User } from '../../common/lib/user'
@ -38,9 +41,9 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
} }
} }
) )
// TODO: switch to prod id const fromUserId = isProd()
// const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account ? HOUSE_LIQUIDITY_PROVIDER_ID
const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account : DEV_HOUSE_LIQUIDITY_PROVIDER_ID
const fromSnap = await firestore.doc(`users/${fromUserId}`).get() const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
if (!fromSnap.exists) throw new APIError(400, 'From user not found.') if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
const fromUser = fromSnap.data() as User const fromUser = fromSnap.data() as User

View File

@ -29,6 +29,7 @@ export * from './on-update-group'
export * from './on-create-group' export * from './on-create-group'
export * from './on-update-user' export * from './on-update-user'
export * from './on-create-comment-on-group' export * from './on-create-comment-on-group'
export * from './on-create-txn'
// v2 // v2
export * from './health' export * from './health'

View File

@ -0,0 +1,68 @@
import * as functions from 'firebase-functions'
import { Txn } from 'common/txn'
import { getContract, getUser, log } from './utils'
import { createNotification } from './create-notification'
import * as admin from 'firebase-admin'
import { Comment } from 'common/comment'
const firestore = admin.firestore()
export const onCreateTxn = functions.firestore
.document('txns/{txnId}')
.onCreate(async (change, context) => {
const txn = change.data() as Txn
const { eventId } = context
if (txn.category === 'TIP') {
await handleTipTxn(txn, eventId)
}
})
async function handleTipTxn(txn: Txn, eventId: string) {
// get user sending and receiving tip
const [sender, receiver] = await Promise.all([
getUser(txn.fromId),
getUser(txn.toId),
])
if (!sender || !receiver) {
log('Could not find corresponding users')
return
}
if (!txn.data?.contractId || !txn.data?.commentId) {
log('No contractId or comment id in tip txn.data')
return
}
const contract = await getContract(txn.data.contractId)
if (!contract) {
log('Could not find contract')
return
}
const commentSnapshot = await firestore
.collection('contracts')
.doc(contract.id)
.collection('comments')
.doc(txn.data.commentId)
.get()
if (!commentSnapshot.exists) {
log('Could not find comment')
return
}
const comment = commentSnapshot.data() as Comment
await createNotification(
txn.id,
'tip',
'created',
sender,
eventId,
txn.amount.toString(),
contract,
'comment',
receiver.id,
txn.data?.commentId,
comment.text
)
}

View File

@ -53,7 +53,7 @@ export function EmptyAvatar(props: { size?: number; multi?: boolean }) {
return ( return (
<div <div
className={`flex h-${size} w-${size} items-center justify-center rounded-full bg-gray-200`} className={`flex flex-shrink-0 h-${size} w-${size} items-center justify-center rounded-full bg-gray-200`}
> >
<Icon className={`h-${insize} w-${insize} text-gray-500`} aria-hidden /> <Icon className={`h-${insize} w-${insize} text-gray-500`} aria-hidden />
</div> </div>

View File

@ -224,7 +224,7 @@ export function FeedComment(props: {
return ( return (
<Row <Row
className={clsx( className={clsx(
'flex space-x-1.5 transition-all duration-1000 sm:space-x-3', 'flex space-x-1.5 sm:space-x-3',
highlighted ? `-m-1 rounded bg-indigo-500/[0.2] p-2` : '' highlighted ? `-m-1 rounded bg-indigo-500/[0.2] p-2` : ''
)} )}
> >

View File

@ -39,17 +39,19 @@ export function groupNotifications(notifications: Notification[]) {
) )
Object.keys(notificationGroupsByDay).forEach((day) => { Object.keys(notificationGroupsByDay).forEach((day) => {
const notificationsGroupedByDay = notificationGroupsByDay[day] const notificationsGroupedByDay = notificationGroupsByDay[day]
const bonusNotifications = notificationsGroupedByDay.filter( const incomeNotifications = notificationsGroupedByDay.filter(
(notification) => notification.sourceType === 'bonus' (notification) =>
notification.sourceType === 'bonus' || notification.sourceType === 'tip'
) )
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter( const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
(notification) => notification.sourceType !== 'bonus' (notification) =>
notification.sourceType !== 'bonus' && notification.sourceType !== 'tip'
) )
if (bonusNotifications.length > 0) { if (incomeNotifications.length > 0) {
notificationGroups = notificationGroups.concat({ notificationGroups = notificationGroups.concat({
notifications: bonusNotifications, notifications: incomeNotifications,
groupedById: 'income' + day, groupedById: 'income' + day,
isSeen: bonusNotifications[0].isSeen, isSeen: incomeNotifications[0].isSeen,
timePeriod: day, timePeriod: day,
type: 'income', type: 'income',
}) })

View File

@ -1,7 +1,7 @@
import { Tabs } from 'web/components/layout/tabs' import { Tabs } from 'web/components/layout/tabs'
import { useUser } from 'web/hooks/use-user' import { useUser } from 'web/hooks/use-user'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Notification } from 'common/notification' import { Notification, notification_source_types } from 'common/notification'
import { Avatar, EmptyAvatar } from 'web/components/avatar' import { Avatar, EmptyAvatar } from 'web/components/avatar'
import { Row } from 'web/components/layout/row' import { Row } from 'web/components/layout/row'
import { Page } from 'web/components/page' import { Page } from 'web/components/page'
@ -34,10 +34,10 @@ import toast from 'react-hot-toast'
import { formatMoney } from 'common/util/format' import { formatMoney } from 'common/util/format'
import { groupPath } from 'web/lib/firebase/groups' import { groupPath } from 'web/lib/firebase/groups'
import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants' import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
import { groupBy } from 'lodash' import { groupBy, sum, uniq } from 'lodash'
export const NOTIFICATIONS_PER_PAGE = 30 export const NOTIFICATIONS_PER_PAGE = 30
export const HIGHLIGHT_DURATION = 30 * 1000 const MULTIPLE_USERS_KEY = 'multipleUsers'
export default function Notifications() { export default function Notifications() {
const user = useUser() const user = useUser()
@ -187,16 +187,12 @@ function IncomeNotificationGroupItem(props: {
const { notificationGroup, className } = props const { notificationGroup, className } = props
const { notifications } = notificationGroup const { notifications } = notificationGroup
const numSummaryLines = 3 const numSummaryLines = 3
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const [highlighted, setHighlighted] = useState(false) const [highlighted, setHighlighted] = useState(
notifications.some((n) => !n.isSeen)
)
useEffect(() => { useEffect(() => {
if (notifications.some((n) => !n.isSeen)) {
setHighlighted(true)
setTimeout(() => {
setHighlighted(false)
}, HIGHLIGHT_DURATION)
}
setNotificationsAsSeen(notifications) setNotificationsAsSeen(notifications)
}, [notifications]) }, [notifications])
@ -204,51 +200,62 @@ function IncomeNotificationGroupItem(props: {
if (expanded) setHighlighted(false) if (expanded) setHighlighted(false)
}, [expanded]) }, [expanded])
const totalIncome = notifications.reduce( const totalIncome = sum(
(acc, notification) => notifications.map((notification) =>
acc + notification.sourceText ? parseInt(notification.sourceText) : 0
(notification.sourceType && )
notification.sourceText &&
notification.sourceType === 'bonus'
? parseInt(notification.sourceText)
: 0),
0
) )
// loop through the contracts and combine the notification items into one // Loop through the contracts and combine the notification items into one
function combineNotificationsByAddingSourceTextsAndReturningTheRest( function combineNotificationsByAddingNumericSourceTexts(
notifications: Notification[] notifications: Notification[]
) { ) {
const newNotifications = [] const newNotifications = []
const groupedNotificationsByContractId = groupBy( const groupedNotificationsBySourceType = groupBy(
notifications, notifications,
(notification) => { (n) => n.sourceType
return notification.sourceContractId
}
) )
for (const contractId in groupedNotificationsByContractId) { for (const sourceType in groupedNotificationsBySourceType) {
const notificationsForContractId = const groupedNotificationsByContractId = groupBy(
groupedNotificationsByContractId[contractId] groupedNotificationsBySourceType[sourceType],
let sum = 0 (notification) => {
notificationsForContractId.forEach( return notification.sourceContractId
(notification) => }
notification.sourceText &&
(sum = parseInt(notification.sourceText) + sum)
) )
for (const contractId in groupedNotificationsByContractId) {
const notificationsForContractId =
groupedNotificationsByContractId[contractId]
if (notificationsForContractId.length === 1) {
newNotifications.push(notificationsForContractId[0])
continue
}
let sum = 0
notificationsForContractId.forEach(
(notification) =>
notification.sourceText &&
(sum = parseInt(notification.sourceText) + sum)
)
const uniqueUsers = uniq(
notificationsForContractId.map((notification) => {
return notification.sourceUserUsername
})
)
const newNotification = const newNotification = {
notificationsForContractId.length === 1 ...notificationsForContractId[0],
? notificationsForContractId[0] sourceText: sum.toString(),
: { sourceUserUsername:
...notificationsForContractId[0], uniqueUsers.length > 1
sourceText: sum.toString(), ? MULTIPLE_USERS_KEY
} : notificationsForContractId[0].sourceType,
newNotifications.push(newNotification) }
newNotifications.push(newNotification)
}
} }
return newNotifications return newNotifications
} }
const combinedNotifs = const combinedNotifs =
combineNotificationsByAddingSourceTextsAndReturningTheRest(notifications) combineNotificationsByAddingNumericSourceTexts(notifications)
return ( return (
<div <div
@ -286,53 +293,23 @@ function IncomeNotificationGroupItem(props: {
<div> <div>
<div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}> <div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}>
{' '} {' '}
<div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}> <div
className={clsx(
'mt-1 ml-1 gap-1 whitespace-pre-line',
!expanded ? 'line-clamp-4' : ''
)}
>
{!expanded ? ( {!expanded ? (
<> <>
{combinedNotifs {combinedNotifs
.slice(0, numSummaryLines) .slice(0, numSummaryLines)
.map((notification) => { .map((notification) => (
return ( <IncomeNotificationItem
<Row notification={notification}
className={ justSummary={true}
'items-center text-sm text-gray-500 sm:justify-start' key={notification.id}
} />
key={notification.id} ))}
>
<div
className={
'line-clamp-1 flex-1 overflow-hidden sm:flex'
}
>
<div className={'flex pl-1 sm:pl-0'}>
<div
className={
'inline-flex overflow-hidden text-ellipsis pl-1'
}
>
<div className={'mr-1 text-black'}>
<NotificationTextLabel
contract={null}
defaultText={notification.sourceText ?? ''}
className={'line-clamp-1'}
notification={notification}
justSummary={true}
/>
</div>
<span className={'flex-shrink-0'}>
{getReasonForShowingNotification(
notification,
true
)}
{` on`}
<NotificationLink notification={notification} />
</span>
</div>
</div>
</div>
</Row>
)
})}
<div className={'text-sm text-gray-500 hover:underline '}> <div className={'text-sm text-gray-500 hover:underline '}>
{combinedNotifs.length - numSummaryLines > 0 {combinedNotifs.length - numSummaryLines > 0
? 'And ' + ? 'And ' +
@ -344,7 +321,7 @@ function IncomeNotificationGroupItem(props: {
) : ( ) : (
<> <>
{combinedNotifs.map((notification) => ( {combinedNotifs.map((notification) => (
<NotificationItem <IncomeNotificationItem
notification={notification} notification={notification}
key={notification.id} key={notification.id}
justSummary={false} justSummary={false}
@ -361,28 +338,130 @@ function IncomeNotificationGroupItem(props: {
) )
} }
function IncomeNotificationItem(props: {
notification: Notification
justSummary?: boolean
}) {
const { notification, justSummary } = props
const {
sourceType,
sourceUserName,
reason,
sourceUserUsername,
createdTime,
} = notification
const [highlighted] = useState(!notification.isSeen)
useEffect(() => {
setNotificationsAsSeen([notification])
}, [notification])
function getReasonForShowingIncomeNotification(simple: boolean) {
const { sourceText } = notification
let reasonText = ''
if (sourceType === 'bonus' && sourceText) {
reasonText = !simple
? `bonus for ${
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
} unique bettors`
: ' bonus for unique bettors on'
} else if (sourceType === 'tip') {
reasonText = !simple ? `tipped you` : `in tips on`
}
return <span className={'flex-shrink-0'}>{reasonText}</span>
}
if (justSummary) {
return (
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
<div className={'line-clamp-1 flex-1 overflow-hidden sm:flex'}>
<div className={'flex pl-1 sm:pl-0'}>
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
<div className={'mr-1 text-black'}>
<NotificationTextLabel
contract={null}
defaultText={notification.sourceText ?? ''}
className={'line-clamp-1'}
notification={notification}
justSummary={true}
/>
</div>
<span className={'flex truncate'}>
{getReasonForShowingIncomeNotification(true)}
<NotificationLink notification={notification} />
</span>
</div>
</div>
</div>
</Row>
)
}
return (
<div
className={clsx(
'bg-white px-2 pt-6 text-sm sm:px-4',
highlighted && 'bg-indigo-200 hover:bg-indigo-100'
)}
>
<a href={getSourceUrl(notification)}>
<Row className={'items-center text-gray-500 sm:justify-start'}>
<div className={'flex max-w-xl shrink '}>
{sourceType && reason && (
<div className={'inline'}>
<span className={'mr-1'}>
<NotificationTextLabel
contract={null}
defaultText={notification.sourceText ?? ''}
notification={notification}
/>
</span>
{sourceType != 'bonus' &&
(sourceUserUsername === MULTIPLE_USERS_KEY ? (
<span className={'mr-1 truncate'}>Multiple users</span>
) : (
<UserLink
name={sourceUserName || ''}
username={sourceUserUsername || ''}
className={'mr-1 flex-shrink-0'}
justFirstName={true}
/>
))}
</div>
)}
{getReasonForShowingIncomeNotification(false)}
<span className={'ml-1 flex hidden sm:inline-block'}>
on
<NotificationLink notification={notification} />
</span>
<RelativeTimestamp time={createdTime} />
</div>
</Row>
<span className={'flex truncate text-gray-500 sm:hidden'}>
on
<NotificationLink notification={notification} />
</span>
<div className={'mt-4 border-b border-gray-300'} />
</a>
</div>
)
}
function NotificationGroupItem(props: { function NotificationGroupItem(props: {
notificationGroup: NotificationGroup notificationGroup: NotificationGroup
className?: string className?: string
}) { }) {
const { notificationGroup, className } = props const { notificationGroup, className } = props
const { notifications } = notificationGroup const { notifications } = notificationGroup
const { const { sourceContractTitle } = notifications[0]
sourceContractTitle,
sourceContractSlug,
sourceContractCreatorUsername,
} = notifications[0]
const numSummaryLines = 3 const numSummaryLines = 3
const [expanded, setExpanded] = useState(false) const [expanded, setExpanded] = useState(false)
const [highlighted, setHighlighted] = useState(false) const [highlighted, setHighlighted] = useState(
notifications.some((n) => !n.isSeen)
)
useEffect(() => { useEffect(() => {
if (notifications.some((n) => !n.isSeen)) {
setHighlighted(true)
setTimeout(() => {
setHighlighted(false)
}, HIGHLIGHT_DURATION)
}
setNotificationsAsSeen(notifications) setNotificationsAsSeen(notifications)
}, [notifications]) }, [notifications])
@ -408,27 +487,18 @@ function NotificationGroupItem(props: {
)} )}
<Row className={'items-center text-gray-500 sm:justify-start'}> <Row className={'items-center text-gray-500 sm:justify-start'}>
<EmptyAvatar multi /> <EmptyAvatar multi />
<div className={'flex-1 overflow-hidden pl-2 sm:flex'}> <div className={'flex truncate pl-2'}>
<div <div
onClick={() => setExpanded(!expanded)} onClick={() => setExpanded(!expanded)}
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'} className={' flex cursor-pointer truncate pl-1 sm:pl-0'}
> >
{sourceContractTitle ? ( {sourceContractTitle ? (
<span> <>
{'Activity on '} <span className={'flex-shrink-0'}>{'Activity on '}</span>
<a <span className={'truncate'}>
href={ <NotificationLink notification={notifications[0]} />
sourceContractCreatorUsername </span>
? `/${sourceContractCreatorUsername}/${sourceContractSlug}` </>
: ''
}
className={
'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
}
>
{sourceContractTitle}
</a>
</span>
) : ( ) : (
'Other activity' 'Other activity'
)} )}
@ -439,7 +509,13 @@ function NotificationGroupItem(props: {
<div> <div>
<div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}> <div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}>
{' '} {' '}
<div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}> <div
className={clsx(
'mt-1 ml-1 gap-1 whitespace-pre-line',
!expanded ? 'line-clamp-4' : ''
)}
>
{' '}
{!expanded ? ( {!expanded ? (
<> <>
{notifications.slice(0, numSummaryLines).map((notification) => { {notifications.slice(0, numSummaryLines).map((notification) => {
@ -466,6 +542,7 @@ function NotificationGroupItem(props: {
notification={notification} notification={notification}
key={notification.id} key={notification.id}
justSummary={false} justSummary={false}
hideTitle={true}
/> />
))} ))}
</> </>
@ -695,7 +772,7 @@ function NotificationLink(props: { notification: Notification }) {
: '' : ''
} }
className={ className={
'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2' 'ml-1 inline max-w-xs truncate font-bold text-gray-500 hover:underline hover:decoration-indigo-400 hover:decoration-2 sm:max-w-sm'
} }
> >
{sourceContractTitle || sourceTitle} {sourceContractTitle || sourceTitle}
@ -703,11 +780,54 @@ function NotificationLink(props: { notification: Notification }) {
) )
} }
function getSourceUrl(notification: Notification) {
const {
sourceType,
sourceId,
sourceUserUsername,
sourceContractCreatorUsername,
sourceContractSlug,
sourceSlug,
} = notification
if (sourceType === 'follow') return `/${sourceUserUsername}`
if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}`
if (
sourceContractCreatorUsername &&
sourceContractSlug &&
sourceType === 'user'
)
return `/${sourceContractCreatorUsername}/${sourceContractSlug}`
if (sourceType === 'tip')
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${sourceSlug}`
if (sourceContractCreatorUsername && sourceContractSlug)
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
sourceId ?? '',
sourceType
)}`
}
function getSourceIdForLinkComponent(
sourceId: string,
sourceType?: notification_source_types
) {
switch (sourceType) {
case 'answer':
return `answer-${sourceId}`
case 'comment':
return sourceId
case 'contract':
return ''
default:
return sourceId
}
}
function NotificationItem(props: { function NotificationItem(props: {
notification: Notification notification: Notification
justSummary?: boolean justSummary?: boolean
hideTitle?: boolean
}) { }) {
const { notification, justSummary } = props const { notification, justSummary, hideTitle } = props
const { const {
sourceType, sourceType,
sourceId, sourceId,
@ -721,7 +841,6 @@ function NotificationItem(props: {
sourceText, sourceText,
sourceContractCreatorUsername, sourceContractCreatorUsername,
sourceContractSlug, sourceContractSlug,
sourceSlug,
} = notification } = notification
const [defaultNotificationText, setDefaultNotificationText] = const [defaultNotificationText, setDefaultNotificationText] =
@ -736,48 +855,12 @@ function NotificationItem(props: {
} }
}, [reasonText, sourceText]) }, [reasonText, sourceText])
const [highlighted, setHighlighted] = useState(false) const [highlighted] = useState(!notification.isSeen)
useEffect(() => {
if (!notification.isSeen) {
setHighlighted(true)
setTimeout(() => {
setHighlighted(false)
}, HIGHLIGHT_DURATION)
}
}, [notification.isSeen])
useEffect(() => { useEffect(() => {
setNotificationsAsSeen([notification]) setNotificationsAsSeen([notification])
}, [notification]) }, [notification])
function getSourceUrl() {
if (sourceType === 'follow') return `/${sourceUserUsername}`
if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}`
if (
sourceContractCreatorUsername &&
sourceContractSlug &&
sourceType === 'user'
)
return `/${sourceContractCreatorUsername}/${sourceContractSlug}`
if (sourceContractCreatorUsername && sourceContractSlug)
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
sourceId ?? ''
)}`
}
function getSourceIdForLinkComponent(sourceId: string) {
switch (sourceType) {
case 'answer':
return `answer-${sourceId}`
case 'comment':
return sourceId
case 'contract':
return ''
default:
return sourceId
}
}
if (justSummary) { if (justSummary) {
return ( return (
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}> <Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
@ -793,10 +876,7 @@ function NotificationItem(props: {
<span className={'flex-shrink-0'}> <span className={'flex-shrink-0'}>
{sourceType && {sourceType &&
reason && reason &&
getReasonForShowingNotification(notification, true).replace( getReasonForShowingNotification(notification, true, true)}
' on',
''
)}
</span> </span>
<div className={'ml-1 text-black'}> <div className={'ml-1 text-black'}>
<NotificationTextLabel <NotificationTextLabel
@ -821,25 +901,21 @@ function NotificationItem(props: {
highlighted && 'bg-indigo-200 hover:bg-indigo-100' highlighted && 'bg-indigo-200 hover:bg-indigo-100'
)} )}
> >
<a href={getSourceUrl()}> <a href={getSourceUrl(notification)}>
<Row className={'items-center text-gray-500 sm:justify-start'}> <Row className={'items-center text-gray-500 sm:justify-start'}>
{sourceType != 'bonus' ? ( <Avatar
<Avatar avatarUrl={sourceUserAvatarUrl}
avatarUrl={sourceUserAvatarUrl} size={'sm'}
size={'sm'} className={'mr-2'}
className={'mr-2'} username={sourceUserName}
username={sourceUserName} />
/>
) : (
<TrendingUpIcon className={'text-primary h-7 w-7'} />
)}
<div className={'flex-1 overflow-hidden sm:flex'}> <div className={'flex-1 overflow-hidden sm:flex'}>
<div <div
className={ className={
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0' 'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
} }
> >
{sourceType != 'bonus' && sourceUpdateType != 'closed' && ( {sourceUpdateType != 'closed' && (
<UserLink <UserLink
name={sourceUserName || ''} name={sourceUserName || ''}
username={sourceUserUsername || ''} username={sourceUserUsername || ''}
@ -847,26 +923,30 @@ function NotificationItem(props: {
justFirstName={true} justFirstName={true}
/> />
)} )}
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> {sourceType && reason && (
{sourceType && reason && ( <div className={'inline flex truncate'}>
<div className={'inline truncate'}> <span className={'ml-1 flex-shrink-0'}>
{getReasonForShowingNotification(notification, false)} {getReasonForShowingNotification(notification, false, true)}
</span>
{!hideTitle && (
<NotificationLink notification={notification} /> <NotificationLink notification={notification} />
</div> )}
)} </div>
</div> )}
{sourceId &&
sourceContractSlug &&
sourceContractCreatorUsername ? (
<CopyLinkDateTimeComponent
prefix={sourceContractCreatorUsername}
slug={sourceContractSlug}
createdTime={createdTime}
elementId={getSourceIdForLinkComponent(sourceId)}
className={'-mx-1 inline-flex sm:inline-block'}
/>
) : (
<RelativeTimestamp time={createdTime} />
)}
</div> </div>
{sourceId && sourceContractSlug && sourceContractCreatorUsername ? (
<CopyLinkDateTimeComponent
prefix={sourceContractCreatorUsername}
slug={sourceContractSlug}
createdTime={createdTime}
elementId={getSourceIdForLinkComponent(sourceId)}
className={'-mx-1 inline-flex sm:inline-block'}
/>
) : (
<RelativeTimestamp time={createdTime} />
)}
</div> </div>
</Row> </Row>
<div className={'mt-1 ml-1 md:text-base'}> <div className={'mt-1 ml-1 md:text-base'}>
@ -938,9 +1018,11 @@ function NotificationTextLabel(props: {
return ( return (
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span> <span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
) )
} else if (sourceType === 'bonus' && sourceText) { } else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) {
return ( return (
<span className="text-primary">{formatMoney(parseInt(sourceText))}</span> <span className="text-primary">
{'+' + formatMoney(parseInt(sourceText))}
</span>
) )
} }
// return default text // return default text
@ -953,19 +1035,19 @@ function NotificationTextLabel(props: {
function getReasonForShowingNotification( function getReasonForShowingNotification(
notification: Notification, notification: Notification,
simple?: boolean simple?: boolean,
replaceOn?: boolean
) { ) {
const { sourceType, sourceUpdateType, sourceText, reason, sourceSlug } = const { sourceType, sourceUpdateType, reason, sourceSlug } = notification
notification
let reasonText: string let reasonText: string
switch (sourceType) { switch (sourceType) {
case 'comment': case 'comment':
if (reason === 'reply_to_users_answer') if (reason === 'reply_to_users_answer')
reasonText = !simple ? 'replied to your answer on' : 'replied' reasonText = !simple ? 'replied to you on' : 'replied'
else if (reason === 'tagged_user') else if (reason === 'tagged_user')
reasonText = !simple ? 'tagged you in a comment on' : 'tagged you' reasonText = !simple ? 'tagged you on' : 'tagged you'
else if (reason === 'reply_to_users_comment') else if (reason === 'reply_to_users_comment')
reasonText = !simple ? 'replied to your comment on' : 'replied' reasonText = !simple ? 'replied to you on' : 'replied'
else if (reason === 'on_users_contract') else if (reason === 'on_users_contract')
reasonText = !simple ? `commented on your question` : 'commented' reasonText = !simple ? `commented on your question` : 'commented'
else if (reason === 'on_contract_with_users_comment') else if (reason === 'on_contract_with_users_comment')
@ -973,7 +1055,7 @@ function getReasonForShowingNotification(
else if (reason === 'on_contract_with_users_answer') else if (reason === 'on_contract_with_users_answer')
reasonText = `commented on` reasonText = `commented on`
else if (reason === 'on_contract_with_users_shares_in') else if (reason === 'on_contract_with_users_shares_in')
reasonText = `commented` reasonText = `commented on`
else reasonText = `commented on` else reasonText = `commented on`
break break
case 'contract': case 'contract':
@ -1008,17 +1090,13 @@ function getReasonForShowingNotification(
else if (sourceSlug) reasonText = 'joined because you shared' else if (sourceSlug) reasonText = 'joined because you shared'
else reasonText = 'joined because of you' else reasonText = 'joined because of you'
break break
case 'bonus':
if (reason === 'unique_bettors_on_your_contract' && sourceText)
reasonText = !simple
? `You had ${
parseInt(sourceText) / UNIQUE_BETTOR_BONUS_AMOUNT
} unique bettors on`
: ' for unique bettors'
else reasonText = 'You earned your daily manna'
break
default: default:
reasonText = '' reasonText = ''
} }
return reasonText
return (
<span className={'flex-shrink-0'}>
{replaceOn ? reasonText.replace(' on', '') : reasonText}
</span>
)
} }