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:
parent
2d1e76eae8
commit
de20ee9fb9
|
@ -15,6 +15,7 @@ import { ENV_CONFIG } from './envs/constants'
|
|||
export const FIXED_ANTE = ENV_CONFIG.fixedAnte ?? 100
|
||||
|
||||
export const HOUSE_LIQUIDITY_PROVIDER_ID = 'IPTOzEqrpkWmEzh6hwvAyY9PqFb2' // @ManifoldMarkets' id
|
||||
export const DEV_HOUSE_LIQUIDITY_PROVIDER_ID = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // @ManifoldMarkets' id
|
||||
|
||||
export function getCpmmInitialLiquidity(
|
||||
providerId: string,
|
||||
|
|
|
@ -61,3 +61,4 @@ export type notification_reason_types =
|
|||
| 'user_joined_to_bet_on_your_market'
|
||||
| 'unique_bettors_on_your_contract'
|
||||
| 'on_group_you_are_member_of'
|
||||
| 'tip_received'
|
||||
|
|
|
@ -66,9 +66,7 @@ export const createNotification = async (
|
|||
sourceUserAvatarUrl: sourceUser.avatarUrl,
|
||||
sourceText,
|
||||
sourceContractCreatorUsername: sourceContract?.creatorUsername,
|
||||
// TODO: move away from sourceContractTitle to sourceTitle
|
||||
sourceContractTitle: sourceContract?.question,
|
||||
// TODO: move away from sourceContractSlug to sourceSlug
|
||||
sourceContractSlug: sourceContract?.slug,
|
||||
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
|
||||
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
|
||||
|
@ -278,13 +276,22 @@ export const createNotification = 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,
|
||||
userId: string
|
||||
) => {
|
||||
if (shouldGetNotification(userId, userToReasonTexts))
|
||||
userToReasonTexts[userId] = {
|
||||
reason: 'on_group_you_are_member_of',
|
||||
isSeeOnHref: sourceSlug,
|
||||
reason: 'tip_received',
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -304,6 +311,7 @@ export const createNotification = async (
|
|||
|
||||
// The following functions need sourceContract to be defined.
|
||||
if (!sourceContract) return userToReasonTexts
|
||||
|
||||
if (
|
||||
sourceType === 'comment' ||
|
||||
sourceType === 'answer' ||
|
||||
|
@ -338,6 +346,8 @@ export const createNotification = async (
|
|||
userToReasonTexts,
|
||||
sourceContract.creatorId
|
||||
)
|
||||
} else if (sourceType === 'tip' && relatedUserId) {
|
||||
await notifyTippedUserOfNewTip(userToReasonTexts, relatedUserId)
|
||||
}
|
||||
return userToReasonTexts
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import { APIError, newEndpoint } from './api'
|
||||
import { log } from './utils'
|
||||
import { isProd, log } from './utils'
|
||||
import * as admin from 'firebase-admin'
|
||||
import { PrivateUser } from '../../common/lib/user'
|
||||
import { uniq } from 'lodash'
|
||||
import { Bet } from '../../common/lib/bet'
|
||||
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 { createNotification } from './create-notification'
|
||||
import { User } from '../../common/lib/user'
|
||||
|
@ -38,9 +41,9 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
|||
}
|
||||
}
|
||||
)
|
||||
// TODO: switch to prod id
|
||||
// const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
|
||||
const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account
|
||||
const fromUserId = isProd()
|
||||
? HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
: DEV_HOUSE_LIQUIDITY_PROVIDER_ID
|
||||
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
||||
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
|
||||
const fromUser = fromSnap.data() as User
|
||||
|
|
|
@ -29,6 +29,7 @@ export * from './on-update-group'
|
|||
export * from './on-create-group'
|
||||
export * from './on-update-user'
|
||||
export * from './on-create-comment-on-group'
|
||||
export * from './on-create-txn'
|
||||
|
||||
// v2
|
||||
export * from './health'
|
||||
|
|
68
functions/src/on-create-txn.ts
Normal file
68
functions/src/on-create-txn.ts
Normal 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
|
||||
)
|
||||
}
|
|
@ -53,7 +53,7 @@ export function EmptyAvatar(props: { size?: number; multi?: boolean }) {
|
|||
|
||||
return (
|
||||
<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 />
|
||||
</div>
|
||||
|
|
|
@ -224,7 +224,7 @@ export function FeedComment(props: {
|
|||
return (
|
||||
<Row
|
||||
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` : ''
|
||||
)}
|
||||
>
|
||||
|
|
|
@ -39,17 +39,19 @@ export function groupNotifications(notifications: Notification[]) {
|
|||
)
|
||||
Object.keys(notificationGroupsByDay).forEach((day) => {
|
||||
const notificationsGroupedByDay = notificationGroupsByDay[day]
|
||||
const bonusNotifications = notificationsGroupedByDay.filter(
|
||||
(notification) => notification.sourceType === 'bonus'
|
||||
const incomeNotifications = notificationsGroupedByDay.filter(
|
||||
(notification) =>
|
||||
notification.sourceType === 'bonus' || notification.sourceType === 'tip'
|
||||
)
|
||||
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({
|
||||
notifications: bonusNotifications,
|
||||
notifications: incomeNotifications,
|
||||
groupedById: 'income' + day,
|
||||
isSeen: bonusNotifications[0].isSeen,
|
||||
isSeen: incomeNotifications[0].isSeen,
|
||||
timePeriod: day,
|
||||
type: 'income',
|
||||
})
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Tabs } from 'web/components/layout/tabs'
|
||||
import { useUser } from 'web/hooks/use-user'
|
||||
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 { Row } from 'web/components/layout/row'
|
||||
import { Page } from 'web/components/page'
|
||||
|
@ -34,10 +34,10 @@ import toast from 'react-hot-toast'
|
|||
import { formatMoney } from 'common/util/format'
|
||||
import { groupPath } from 'web/lib/firebase/groups'
|
||||
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 HIGHLIGHT_DURATION = 30 * 1000
|
||||
const MULTIPLE_USERS_KEY = 'multipleUsers'
|
||||
|
||||
export default function Notifications() {
|
||||
const user = useUser()
|
||||
|
@ -187,16 +187,12 @@ function IncomeNotificationGroupItem(props: {
|
|||
const { notificationGroup, className } = props
|
||||
const { notifications } = notificationGroup
|
||||
const numSummaryLines = 3
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [highlighted, setHighlighted] = useState(false)
|
||||
const [highlighted, setHighlighted] = useState(
|
||||
notifications.some((n) => !n.isSeen)
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (notifications.some((n) => !n.isSeen)) {
|
||||
setHighlighted(true)
|
||||
setTimeout(() => {
|
||||
setHighlighted(false)
|
||||
}, HIGHLIGHT_DURATION)
|
||||
}
|
||||
setNotificationsAsSeen(notifications)
|
||||
}, [notifications])
|
||||
|
||||
|
@ -204,23 +200,23 @@ function IncomeNotificationGroupItem(props: {
|
|||
if (expanded) setHighlighted(false)
|
||||
}, [expanded])
|
||||
|
||||
const totalIncome = notifications.reduce(
|
||||
(acc, notification) =>
|
||||
acc +
|
||||
(notification.sourceType &&
|
||||
notification.sourceText &&
|
||||
notification.sourceType === 'bonus'
|
||||
? parseInt(notification.sourceText)
|
||||
: 0),
|
||||
0
|
||||
const totalIncome = sum(
|
||||
notifications.map((notification) =>
|
||||
notification.sourceText ? parseInt(notification.sourceText) : 0
|
||||
)
|
||||
// loop through the contracts and combine the notification items into one
|
||||
function combineNotificationsByAddingSourceTextsAndReturningTheRest(
|
||||
)
|
||||
// Loop through the contracts and combine the notification items into one
|
||||
function combineNotificationsByAddingNumericSourceTexts(
|
||||
notifications: Notification[]
|
||||
) {
|
||||
const newNotifications = []
|
||||
const groupedNotificationsByContractId = groupBy(
|
||||
const groupedNotificationsBySourceType = groupBy(
|
||||
notifications,
|
||||
(n) => n.sourceType
|
||||
)
|
||||
for (const sourceType in groupedNotificationsBySourceType) {
|
||||
const groupedNotificationsByContractId = groupBy(
|
||||
groupedNotificationsBySourceType[sourceType],
|
||||
(notification) => {
|
||||
return notification.sourceContractId
|
||||
}
|
||||
|
@ -228,27 +224,38 @@ function IncomeNotificationGroupItem(props: {
|
|||
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 =
|
||||
notificationsForContractId.length === 1
|
||||
? notificationsForContractId[0]
|
||||
: {
|
||||
const newNotification = {
|
||||
...notificationsForContractId[0],
|
||||
sourceText: sum.toString(),
|
||||
sourceUserUsername:
|
||||
uniqueUsers.length > 1
|
||||
? MULTIPLE_USERS_KEY
|
||||
: notificationsForContractId[0].sourceType,
|
||||
}
|
||||
newNotifications.push(newNotification)
|
||||
}
|
||||
}
|
||||
return newNotifications
|
||||
}
|
||||
|
||||
const combinedNotifs =
|
||||
combineNotificationsByAddingSourceTextsAndReturningTheRest(notifications)
|
||||
combineNotificationsByAddingNumericSourceTexts(notifications)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -286,53 +293,23 @@ function IncomeNotificationGroupItem(props: {
|
|||
<div>
|
||||
<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 ? (
|
||||
<>
|
||||
{combinedNotifs
|
||||
.slice(0, numSummaryLines)
|
||||
.map((notification) => {
|
||||
return (
|
||||
<Row
|
||||
className={
|
||||
'items-center text-sm text-gray-500 sm:justify-start'
|
||||
}
|
||||
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'}
|
||||
.map((notification) => (
|
||||
<IncomeNotificationItem
|
||||
notification={notification}
|
||||
justSummary={true}
|
||||
key={notification.id}
|
||||
/>
|
||||
</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 '}>
|
||||
{combinedNotifs.length - numSummaryLines > 0
|
||||
? 'And ' +
|
||||
|
@ -344,7 +321,7 @@ function IncomeNotificationGroupItem(props: {
|
|||
) : (
|
||||
<>
|
||||
{combinedNotifs.map((notification) => (
|
||||
<NotificationItem
|
||||
<IncomeNotificationItem
|
||||
notification={notification}
|
||||
key={notification.id}
|
||||
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: {
|
||||
notificationGroup: NotificationGroup
|
||||
className?: string
|
||||
}) {
|
||||
const { notificationGroup, className } = props
|
||||
const { notifications } = notificationGroup
|
||||
const {
|
||||
sourceContractTitle,
|
||||
sourceContractSlug,
|
||||
sourceContractCreatorUsername,
|
||||
} = notifications[0]
|
||||
const { sourceContractTitle } = notifications[0]
|
||||
const numSummaryLines = 3
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [highlighted, setHighlighted] = useState(false)
|
||||
const [highlighted, setHighlighted] = useState(
|
||||
notifications.some((n) => !n.isSeen)
|
||||
)
|
||||
useEffect(() => {
|
||||
if (notifications.some((n) => !n.isSeen)) {
|
||||
setHighlighted(true)
|
||||
setTimeout(() => {
|
||||
setHighlighted(false)
|
||||
}, HIGHLIGHT_DURATION)
|
||||
}
|
||||
setNotificationsAsSeen(notifications)
|
||||
}, [notifications])
|
||||
|
||||
|
@ -408,27 +487,18 @@ function NotificationGroupItem(props: {
|
|||
)}
|
||||
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
||||
<EmptyAvatar multi />
|
||||
<div className={'flex-1 overflow-hidden pl-2 sm:flex'}>
|
||||
<div className={'flex truncate pl-2'}>
|
||||
<div
|
||||
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 ? (
|
||||
<span>
|
||||
{'Activity on '}
|
||||
<a
|
||||
href={
|
||||
sourceContractCreatorUsername
|
||||
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
||||
: ''
|
||||
}
|
||||
className={
|
||||
'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
||||
}
|
||||
>
|
||||
{sourceContractTitle}
|
||||
</a>
|
||||
<>
|
||||
<span className={'flex-shrink-0'}>{'Activity on '}</span>
|
||||
<span className={'truncate'}>
|
||||
<NotificationLink notification={notifications[0]} />
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
'Other activity'
|
||||
)}
|
||||
|
@ -439,7 +509,13 @@ function NotificationGroupItem(props: {
|
|||
<div>
|
||||
<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 ? (
|
||||
<>
|
||||
{notifications.slice(0, numSummaryLines).map((notification) => {
|
||||
|
@ -466,6 +542,7 @@ function NotificationGroupItem(props: {
|
|||
notification={notification}
|
||||
key={notification.id}
|
||||
justSummary={false}
|
||||
hideTitle={true}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -695,7 +772,7 @@ function NotificationLink(props: { notification: Notification }) {
|
|||
: ''
|
||||
}
|
||||
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}
|
||||
|
@ -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: {
|
||||
notification: Notification
|
||||
justSummary?: boolean
|
||||
hideTitle?: boolean
|
||||
}) {
|
||||
const { notification, justSummary } = props
|
||||
const { notification, justSummary, hideTitle } = props
|
||||
const {
|
||||
sourceType,
|
||||
sourceId,
|
||||
|
@ -721,7 +841,6 @@ function NotificationItem(props: {
|
|||
sourceText,
|
||||
sourceContractCreatorUsername,
|
||||
sourceContractSlug,
|
||||
sourceSlug,
|
||||
} = notification
|
||||
|
||||
const [defaultNotificationText, setDefaultNotificationText] =
|
||||
|
@ -736,48 +855,12 @@ function NotificationItem(props: {
|
|||
}
|
||||
}, [reasonText, sourceText])
|
||||
|
||||
const [highlighted, setHighlighted] = useState(false)
|
||||
useEffect(() => {
|
||||
if (!notification.isSeen) {
|
||||
setHighlighted(true)
|
||||
setTimeout(() => {
|
||||
setHighlighted(false)
|
||||
}, HIGHLIGHT_DURATION)
|
||||
}
|
||||
}, [notification.isSeen])
|
||||
const [highlighted] = useState(!notification.isSeen)
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationsAsSeen([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) {
|
||||
return (
|
||||
<Row className={'items-center text-sm text-gray-500 sm:justify-start'}>
|
||||
|
@ -793,10 +876,7 @@ function NotificationItem(props: {
|
|||
<span className={'flex-shrink-0'}>
|
||||
{sourceType &&
|
||||
reason &&
|
||||
getReasonForShowingNotification(notification, true).replace(
|
||||
' on',
|
||||
''
|
||||
)}
|
||||
getReasonForShowingNotification(notification, true, true)}
|
||||
</span>
|
||||
<div className={'ml-1 text-black'}>
|
||||
<NotificationTextLabel
|
||||
|
@ -821,25 +901,21 @@ function NotificationItem(props: {
|
|||
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'}>
|
||||
{sourceType != 'bonus' ? (
|
||||
<Avatar
|
||||
avatarUrl={sourceUserAvatarUrl}
|
||||
size={'sm'}
|
||||
className={'mr-2'}
|
||||
username={sourceUserName}
|
||||
/>
|
||||
) : (
|
||||
<TrendingUpIcon className={'text-primary h-7 w-7'} />
|
||||
)}
|
||||
<div className={'flex-1 overflow-hidden sm:flex'}>
|
||||
<div
|
||||
className={
|
||||
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
|
||||
}
|
||||
>
|
||||
{sourceType != 'bonus' && sourceUpdateType != 'closed' && (
|
||||
{sourceUpdateType != 'closed' && (
|
||||
<UserLink
|
||||
name={sourceUserName || ''}
|
||||
username={sourceUserUsername || ''}
|
||||
|
@ -847,16 +923,19 @@ function NotificationItem(props: {
|
|||
justFirstName={true}
|
||||
/>
|
||||
)}
|
||||
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
||||
{sourceType && reason && (
|
||||
<div className={'inline truncate'}>
|
||||
{getReasonForShowingNotification(notification, false)}
|
||||
<div className={'inline flex truncate'}>
|
||||
<span className={'ml-1 flex-shrink-0'}>
|
||||
{getReasonForShowingNotification(notification, false, true)}
|
||||
</span>
|
||||
{!hideTitle && (
|
||||
<NotificationLink notification={notification} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{sourceId && sourceContractSlug && sourceContractCreatorUsername ? (
|
||||
)}
|
||||
{sourceId &&
|
||||
sourceContractSlug &&
|
||||
sourceContractCreatorUsername ? (
|
||||
<CopyLinkDateTimeComponent
|
||||
prefix={sourceContractCreatorUsername}
|
||||
slug={sourceContractSlug}
|
||||
|
@ -868,6 +947,7 @@ function NotificationItem(props: {
|
|||
<RelativeTimestamp time={createdTime} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
<div className={'mt-1 ml-1 md:text-base'}>
|
||||
<NotificationTextLabel
|
||||
|
@ -938,9 +1018,11 @@ function NotificationTextLabel(props: {
|
|||
return (
|
||||
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
||||
)
|
||||
} else if (sourceType === 'bonus' && sourceText) {
|
||||
} else if ((sourceType === 'bonus' || sourceType === 'tip') && sourceText) {
|
||||
return (
|
||||
<span className="text-primary">{formatMoney(parseInt(sourceText))}</span>
|
||||
<span className="text-primary">
|
||||
{'+' + formatMoney(parseInt(sourceText))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
// return default text
|
||||
|
@ -953,19 +1035,19 @@ function NotificationTextLabel(props: {
|
|||
|
||||
function getReasonForShowingNotification(
|
||||
notification: Notification,
|
||||
simple?: boolean
|
||||
simple?: boolean,
|
||||
replaceOn?: boolean
|
||||
) {
|
||||
const { sourceType, sourceUpdateType, sourceText, reason, sourceSlug } =
|
||||
notification
|
||||
const { sourceType, sourceUpdateType, reason, sourceSlug } = notification
|
||||
let reasonText: string
|
||||
switch (sourceType) {
|
||||
case 'comment':
|
||||
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')
|
||||
reasonText = !simple ? 'tagged you in a comment on' : 'tagged you'
|
||||
reasonText = !simple ? 'tagged you on' : 'tagged you'
|
||||
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')
|
||||
reasonText = !simple ? `commented on your question` : 'commented'
|
||||
else if (reason === 'on_contract_with_users_comment')
|
||||
|
@ -973,7 +1055,7 @@ function getReasonForShowingNotification(
|
|||
else if (reason === 'on_contract_with_users_answer')
|
||||
reasonText = `commented on`
|
||||
else if (reason === 'on_contract_with_users_shares_in')
|
||||
reasonText = `commented`
|
||||
reasonText = `commented on`
|
||||
else reasonText = `commented on`
|
||||
break
|
||||
case 'contract':
|
||||
|
@ -1008,17 +1090,13 @@ function getReasonForShowingNotification(
|
|||
else if (sourceSlug) reasonText = 'joined because you shared'
|
||||
else reasonText = 'joined because of you'
|
||||
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:
|
||||
reasonText = ''
|
||||
}
|
||||
return reasonText
|
||||
|
||||
return (
|
||||
<span className={'flex-shrink-0'}>
|
||||
{replaceOn ? reasonText.replace(' on', '') : reasonText}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user