From 07f2d390e58c0a0fb90dd0f53b76ab5f1baacda9 Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Mon, 6 Jun 2022 10:54:25 -0600 Subject: [PATCH] Revert "Notification detail, grouping, and settings control [wip] (#403)" This reverts commit 37c7f909a31bcc37bbbdc31015c057d4eaf48b4e. --- common/notification.ts | 21 +- common/user.ts | 3 - functions/src/create-notification.ts | 274 ++++----- functions/src/index.ts | 1 - functions/src/on-create-answer.ts | 4 +- functions/src/on-create-comment.ts | 30 +- functions/src/on-follow-user.ts | 28 - functions/src/on-update-contract.ts | 8 +- web/components/choices-toggle-group.tsx | 4 +- web/components/notifications-icon.tsx | 22 +- web/hooks/use-notifications.ts | 66 --- web/pages/notifications.tsx | 714 +++--------------------- 12 files changed, 211 insertions(+), 964 deletions(-) delete mode 100644 functions/src/on-follow-user.ts delete mode 100644 web/hooks/use-notifications.ts diff --git a/common/notification.ts b/common/notification.ts index 9ccf6f98..f13a75e9 100644 --- a/common/notification.ts +++ b/common/notification.ts @@ -9,7 +9,6 @@ export type Notification = { sourceId?: string sourceType?: notification_source_types - sourceUpdateType?: notification_source_update_types sourceContractId?: string sourceUserName?: string sourceUserUsername?: string @@ -21,24 +20,10 @@ export type notification_source_types = | 'bet' | 'answer' | 'liquidity' - | 'follow' - | 'tip' - | 'admin_message' -export type notification_source_update_types = +export type notification_reason_types = | 'created' | 'updated' | 'resolved' - | 'deleted' - | 'closed' - -export type notification_reason_types = - | 'tagged_user' - | 'on_users_contract' - | 'on_contract_with_users_shares_in' - | 'on_contract_with_users_shares_out' - | 'on_contract_with_users_answer' - | 'on_contract_with_users_comment' - | 'reply_to_users_answer' - | 'reply_to_users_comment' - | 'on_new_follow' + | 'tagged' + | 'replied' diff --git a/common/user.ts b/common/user.ts index 3b74ac1a..b93ed4d6 100644 --- a/common/user.ts +++ b/common/user.ts @@ -36,7 +36,4 @@ export type PrivateUser = { initialDeviceToken?: string initialIpAddress?: string apiKey?: string - notificationPreferences?: notification_subscribe_types } - -export type notification_subscribe_types = 'all' | 'less' | 'none' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 308ed490..fc5e606d 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -2,34 +2,28 @@ import * as admin from 'firebase-admin' import { Notification, notification_reason_types, - notification_source_update_types, notification_source_types, } from '../../common/notification' import { User } from '../../common/user' import { Contract } from '../../common/contract' -import { getUserByUsername, getValues } from './utils' +import { getValues } from './utils' import { Comment } from '../../common/comment' import { uniq } from 'lodash' import { Bet } from '../../common/bet' import { Answer } from '../../common/answer' -import { getContractBetMetrics } from '../../common/calculate' -import { removeUndefinedProps } from '../../common/util/object' const firestore = admin.firestore() type user_to_reason_texts = { - [userId: string]: { reason: notification_reason_types } + [userId: string]: { text: string; reason: notification_reason_types } } export const createNotification = async ( sourceId: string, sourceType: notification_source_types, - sourceUpdateType: notification_source_update_types, + reason: notification_reason_types, + sourceContract: Contract, sourceUser: User, - idempotencyKey: string, - sourceContract?: Contract, - relatedSourceType?: notification_source_types, - relatedUserId?: string, - sourceText?: string + idempotencyKey: string ) => { const shouldGetNotification = ( userId: string, @@ -52,170 +46,128 @@ export const createNotification = async ( const notification: Notification = { id: idempotencyKey, userId, + reasonText: userToReasonTexts[userId].text, reason: userToReasonTexts[userId].reason, createdTime: Date.now(), isSeen: false, sourceId, sourceType, - sourceUpdateType, - sourceContractId: sourceContract?.id, + sourceContractId: sourceContract.id, sourceUserName: sourceUser.name, sourceUserUsername: sourceUser.username, sourceUserAvatarUrl: sourceUser.avatarUrl, } - await notificationRef.set(removeUndefinedProps(notification)) + await notificationRef.set(notification) }) ) } - const notifyRepliedUsers = async ( - userToReasonTexts: user_to_reason_texts - ) => { - if ( - !relatedSourceType || - !relatedUserId || - !shouldGetNotification(relatedUserId, userToReasonTexts) - ) - return - if (relatedSourceType === 'comment') { - userToReasonTexts[relatedUserId] = { - reason: 'reply_to_users_comment', - } - } else if (relatedSourceType === 'answer') { - userToReasonTexts[relatedUserId] = { - reason: 'reply_to_users_answer', - } - } - } - - const notifyFollowedUser = async ( - userToReasonTexts: user_to_reason_texts, - followedUserId: string - ) => { - if (shouldGetNotification(followedUserId, userToReasonTexts)) - userToReasonTexts[followedUserId] = { - reason: 'on_new_follow', - } - } - - const notifyTaggedUsers = async (userToReasonTexts: user_to_reason_texts) => { - if (!sourceText) return - const taggedUsers = sourceText.match(/@\w+/g) - if (!taggedUsers) return - // await all get tagged users: - const users = await Promise.all( - taggedUsers.map(async (username) => { - return await getUserByUsername(username.slice(1)) - }) - ) - users.forEach((taggedUser) => { - if (taggedUser && shouldGetNotification(taggedUser.id, userToReasonTexts)) - userToReasonTexts[taggedUser.id] = { - reason: 'tagged_user', - } - }) - } - - const notifyContractCreator = async ( - userToReasonTexts: user_to_reason_texts, - sourceContract: Contract - ) => { - if (shouldGetNotification(sourceContract.creatorId, userToReasonTexts)) - userToReasonTexts[sourceContract.creatorId] = { - reason: 'on_users_contract', - } - } - - const notifyOtherAnswerersOnContract = async ( - userToReasonTexts: user_to_reason_texts, - sourceContract: Contract - ) => { - const answers = await getValues( - firestore - .collection('contracts') - .doc(sourceContract.id) - .collection('answers') - ) - const recipientUserIds = uniq(answers.map((answer) => answer.userId)) - recipientUserIds.forEach((userId) => { - if (shouldGetNotification(userId, userToReasonTexts)) - userToReasonTexts[userId] = { - reason: 'on_contract_with_users_answer', - } - }) - } - - const notifyOtherCommentersOnContract = async ( - userToReasonTexts: user_to_reason_texts, - sourceContract: Contract - ) => { - const comments = await getValues( - firestore - .collection('contracts') - .doc(sourceContract.id) - .collection('comments') - ) - const recipientUserIds = uniq(comments.map((comment) => comment.userId)) - recipientUserIds.forEach((userId) => { - if (shouldGetNotification(userId, userToReasonTexts)) - userToReasonTexts[userId] = { - reason: 'on_contract_with_users_comment', - } - }) - } - - const notifyOtherBettorsOnContract = async ( - userToReasonTexts: user_to_reason_texts, - sourceContract: Contract - ) => { - const betsSnap = await firestore - .collection(`contracts/${sourceContract.id}/bets`) - .get() - const bets = betsSnap.docs.map((doc) => doc.data() as Bet) - // filter bets for only users that have an amount invested still - const recipientUserIds = uniq(bets.map((bet) => bet.userId)).filter( - (userId) => { - return ( - getContractBetMetrics( - sourceContract, - bets.filter((bet) => bet.userId === userId) - ).invested > 0 - ) - } - ) - recipientUserIds.forEach((userId) => { - if (shouldGetNotification(userId, userToReasonTexts)) - userToReasonTexts[userId] = { - reason: 'on_contract_with_users_shares_in', - } - }) - } - // TODO: Update for liquidity. + // TODO: Find tagged users. + // TODO: Find replies to comments. + // TODO: Filter bets for only open bets. // TODO: Notify users of their own closed but not resolved contracts. - const getUsersToNotify = async () => { - const userToReasonTexts: user_to_reason_texts = {} - // The following functions modify the userToReasonTexts object in place. - if ( - sourceContract && - (sourceType === 'comment' || - sourceType === 'answer' || - sourceType === 'contract') - ) { - if (sourceType === 'comment') { - await notifyRepliedUsers(userToReasonTexts) - await notifyTaggedUsers(userToReasonTexts) - } - await notifyContractCreator(userToReasonTexts, sourceContract) - await notifyOtherAnswerersOnContract(userToReasonTexts, sourceContract) - await notifyOtherBettorsOnContract(userToReasonTexts, sourceContract) - await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract) - } else if (sourceType === 'follow' && relatedUserId) { - await notifyFollowedUser(userToReasonTexts, relatedUserId) - } - return userToReasonTexts - } + if ( + sourceType === 'comment' || + sourceType === 'answer' || + sourceType === 'contract' + ) { + const reasonTextPretext = getReasonTextFromReason(sourceType, reason) - const userToReasonTexts = await getUsersToNotify() - await createUsersNotifications(userToReasonTexts) + const notifyContractCreator = async ( + userToReasonTexts: user_to_reason_texts + ) => { + if (shouldGetNotification(sourceContract.creatorId, userToReasonTexts)) + userToReasonTexts[sourceContract.creatorId] = { + text: `${reasonTextPretext} your question`, + reason, + } + } + + const notifyOtherAnswerersOnContract = async ( + userToReasonTexts: user_to_reason_texts + ) => { + const answers = await getValues( + firestore + .collection('contracts') + .doc(sourceContract.id) + .collection('answers') + ) + const recipientUserIds = uniq(answers.map((answer) => answer.userId)) + recipientUserIds.forEach((userId) => { + if (shouldGetNotification(userId, userToReasonTexts)) + userToReasonTexts[userId] = { + text: `${reasonTextPretext} a question you submitted an answer to`, + reason, + } + }) + } + + const notifyOtherCommentersOnContract = async ( + userToReasonTexts: user_to_reason_texts + ) => { + const comments = await getValues( + firestore + .collection('contracts') + .doc(sourceContract.id) + .collection('comments') + ) + const recipientUserIds = uniq(comments.map((comment) => comment.userId)) + recipientUserIds.forEach((userId) => { + if (shouldGetNotification(userId, userToReasonTexts)) + userToReasonTexts[userId] = { + text: `${reasonTextPretext} a question you commented on`, + reason, + } + }) + } + + const notifyOtherBettorsOnContract = async ( + userToReasonTexts: user_to_reason_texts + ) => { + const betsSnap = await firestore + .collection(`contracts/${sourceContract.id}/bets`) + .get() + const bets = betsSnap.docs.map((doc) => doc.data() as Bet) + const recipientUserIds = uniq(bets.map((bet) => bet.userId)) + recipientUserIds.forEach((userId) => { + if (shouldGetNotification(userId, userToReasonTexts)) + userToReasonTexts[userId] = { + text: `${reasonTextPretext} a question you bet on`, + reason, + } + }) + } + + const getUsersToNotify = async () => { + const userToReasonTexts: user_to_reason_texts = {} + // The following functions modify the userToReasonTexts object in place. + await notifyContractCreator(userToReasonTexts) + await notifyOtherAnswerersOnContract(userToReasonTexts) + await notifyOtherCommentersOnContract(userToReasonTexts) + await notifyOtherBettorsOnContract(userToReasonTexts) + return userToReasonTexts + } + + const userToReasonTexts = await getUsersToNotify() + await createUsersNotifications(userToReasonTexts) + } +} + +function getReasonTextFromReason( + source: notification_source_types, + reason: notification_reason_types +) { + // TODO: Find tagged users. + // TODO: Find replies to comments. + switch (source) { + case 'comment': + return 'commented on' + case 'contract': + return reason + case 'answer': + return 'answered' + default: + throw new Error('Invalid notification reason') + } } diff --git a/functions/src/index.ts b/functions/src/index.ts index 910bd937..81ea59e5 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -30,4 +30,3 @@ export * from './market-close-emails' export * from './add-liquidity' export * from './on-create-answer' export * from './on-update-contract' -export * from './on-follow-user' diff --git a/functions/src/on-create-answer.ts b/functions/src/on-create-answer.ts index 17863205..3fd7fefa 100644 --- a/functions/src/on-create-answer.ts +++ b/functions/src/on-create-answer.ts @@ -25,8 +25,8 @@ export const onCreateAnswer = functions.firestore answer.id, 'answer', 'created', + contract, answerCreator, - eventId, - contract + eventId ) }) diff --git a/functions/src/on-create-comment.ts b/functions/src/on-create-comment.ts index 75bfc6b1..bf1dacd5 100644 --- a/functions/src/on-create-comment.ts +++ b/functions/src/on-create-comment.ts @@ -30,6 +30,15 @@ export const onCreateComment = functions const commentCreator = await getUser(comment.userId) if (!commentCreator) throw new Error('Could not find comment creator') + await createNotification( + comment.id, + 'comment', + 'created', + contract, + commentCreator, + eventId + ) + await firestore .collection('contracts') .doc(contract.id) @@ -62,27 +71,6 @@ export const onCreateComment = functions const comments = await getValues( firestore.collection('contracts').doc(contractId).collection('comments') ) - const relatedSourceType = comment.replyToCommentId - ? 'comment' - : comment.answerOutcome - ? 'answer' - : undefined - - const relatedUser = comment.replyToCommentId - ? comments.find((c) => c.id === comment.replyToCommentId)?.userId - : answer?.userId - - await createNotification( - comment.id, - 'comment', - 'created', - commentCreator, - eventId, - contract, - relatedSourceType, - relatedUser, - comment.text - ) const recipientUserIds = uniq([ contract.creatorId, diff --git a/functions/src/on-follow-user.ts b/functions/src/on-follow-user.ts deleted file mode 100644 index 61c671db..00000000 --- a/functions/src/on-follow-user.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as functions from 'firebase-functions' -import { getUser } from './utils' -import { createNotification } from './create-notification' - -export const onFollowUser = functions.firestore - .document('users/{userId}/follows/{followedUserId}') - .onCreate(async (change, context) => { - const { userId } = context.params as { - userId: string - } - const { eventId } = context - - const follow = change.data() as { userId: string; timestamp: number } - - const followingUser = await getUser(userId) - if (!followingUser) throw new Error('Could not find following user') - - await createNotification( - followingUser.id, - 'follow', - 'created', - followingUser, - eventId, - undefined, - undefined, - follow.userId - ) - }) diff --git a/functions/src/on-update-contract.ts b/functions/src/on-update-contract.ts index 3bf202a2..2ccd5540 100644 --- a/functions/src/on-update-contract.ts +++ b/functions/src/on-update-contract.ts @@ -18,9 +18,9 @@ export const onUpdateContract = functions.firestore contract.id, 'contract', 'resolved', + contract, contractUpdater, - eventId, - contract + eventId ) } else if ( previousValue.closeTime !== contract.closeTime || @@ -30,9 +30,9 @@ export const onUpdateContract = functions.firestore contract.id, 'contract', 'updated', + contract, contractUpdater, - eventId, - contract + eventId ) } }) diff --git a/web/components/choices-toggle-group.tsx b/web/components/choices-toggle-group.tsx index f974d72f..13800ba6 100644 --- a/web/components/choices-toggle-group.tsx +++ b/web/components/choices-toggle-group.tsx @@ -8,7 +8,6 @@ export function ChoicesToggleGroup(props: { isSubmitting?: boolean setChoice: (p: number | string) => void className?: string - toggleClassName?: string children?: React.ReactNode }) { const { @@ -18,7 +17,6 @@ export function ChoicesToggleGroup(props: { choicesMap, className, children, - toggleClassName, } = props return ( () const router = useRouter() useEffect(() => { - if (router.pathname.endsWith('notifications')) return setSeen(true) - else setSeen(false) + if (router.pathname.endsWith('notifications')) return setNotifications([]) }, [router.pathname]) + useEffect(() => { + if (user) return listenForNotifications(user.id, setNotifications, true) + }, [user]) + return (
- {!seen && notifications && notifications.length > 0 && ( -
+ {notifications && notifications.length > 0 && ( +
+ {notifications.length} +
)}
diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts deleted file mode 100644 index 0e303036..00000000 --- a/web/hooks/use-notifications.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useEffect, useState } from 'react' -import { listenForPrivateUser } from 'web/lib/firebase/users' -import { notification_subscribe_types, PrivateUser } from 'common/user' -import { Notification } from 'common/notification' -import { listenForNotifications } from 'web/lib/firebase/notifications' - -export function useNotifications( - userId: string | undefined, - options: { unseenOnly: boolean } -) { - const { unseenOnly } = options - const [privateUser, setPrivateUser] = useState(null) - const [notifications, setNotifications] = useState([]) - const [userAppropriateNotifications, setUserAppropriateNotifications] = - useState([]) - - useEffect(() => { - if (userId) listenForPrivateUser(userId, setPrivateUser) - }, [userId]) - - useEffect(() => { - if (privateUser) - return listenForNotifications( - privateUser.id, - setNotifications, - unseenOnly - ) - }, [privateUser]) - - useEffect(() => { - if (!privateUser) return - - const notificationsToShow = getAppropriateNotifications( - notifications, - privateUser.notificationPreferences - ) - setUserAppropriateNotifications(notificationsToShow) - }, [privateUser, notifications]) - - return userAppropriateNotifications -} - -const lessPriorityReasons = [ - 'on_contract_with_users_comment', - 'on_contract_with_users_answer', - 'on_contract_with_users_shares_out', - // Not sure if users will want to see these w/ less: - // 'on_contract_with_users_shares_in', -] - -function getAppropriateNotifications( - notifications: Notification[], - notificationPreferences?: notification_subscribe_types -) { - if (notificationPreferences === 'all') return notifications - if (notificationPreferences === 'less') - return notifications.filter( - (n) => - n.reason && - // Show all contract notifications - (n.sourceType === 'contract' || !lessPriorityReasons.includes(n.reason)) - ) - if (notificationPreferences === 'none') return [] - - return notifications -} diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 80383425..63c9fc2f 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -6,6 +6,7 @@ import { notification_reason_types, notification_source_types, } from 'common/notification' +import { listenForNotifications } from 'web/lib/firebase/notifications' import { Avatar } from 'web/components/avatar' import { Row } from 'web/components/layout/row' import { Page } from 'web/components/page' @@ -18,90 +19,22 @@ import { Comment } from 'web/lib/firebase/comments' import { getValue } from 'web/lib/firebase/utils' import Custom404 from 'web/pages/404' import { UserLink } from 'web/components/user-page' -import { notification_subscribe_types, PrivateUser } from 'common/user' +import { User } from 'common/user' import { useContract } from 'web/hooks/use-contract' import { Contract } from 'common/contract' -import { ChoicesToggleGroup } from 'web/components/choices-toggle-group' -import { listenForPrivateUser, updatePrivateUser } from 'web/lib/firebase/users' -import { LoadingIndicator } from 'web/components/loading-indicator' -import clsx from 'clsx' -import { groupBy, map } from 'lodash' -import { UsersIcon } from '@heroicons/react/solid' -import { RelativeTimestamp } from 'web/components/relative-timestamp' -import { Linkify } from 'web/components/linkify' -import { - FreeResponseOutcomeLabel, - OutcomeLabel, -} from 'web/components/outcome-label' -import { useNotifications } from 'web/hooks/use-notifications' -import { getContractFromId } from 'web/lib/firebase/contracts' -import { CheckIcon, XIcon } from '@heroicons/react/outline' -import toast from 'react-hot-toast' - -type NotificationGroup = { - notifications: Notification[] - sourceContractId: string - isSeen: boolean - timePeriod: string -} export default function Notifications() { const user = useUser() - const [allNotificationGroups, setAllNotificationsGroups] = useState< - NotificationGroup[] - >([]) - const [unseenNotificationGroups, setUnseenNotificationGroups] = useState< - NotificationGroup[] - >([]) - const notifications = useNotifications(user?.id, { unseenOnly: false }) + const [notifications, setNotifications] = useState< + Notification[] | undefined + >() useEffect(() => { - const notificationIdsToShow = notifications.map( - (notification) => notification.id - ) - // Hide notifications the user doesn't want to see. - const notificationIdsToHide = notifications - .filter( - (notification) => !notificationIdsToShow.includes(notification.id) - ) - .map((notification) => notification.id) + if (user) return listenForNotifications(user.id, setNotifications) + }, [user]) - // Because hidden notifications won't be rendered, set them to seen here - setNotificationsAsSeen( - notifications.filter((n) => notificationIdsToHide.includes(n.id)) - ) - - // Group notifications by contract and 24-hour time period. - const allGroupedNotifications = groupNotifications( - notifications, - notificationIdsToHide - ) - - // Don't add notifications that are already visible or have been seen. - const currentlyVisibleUnseenNotificationIds = Object.values( - unseenNotificationGroups - ) - .map((n) => n.notifications.map((n) => n.id)) - .flat() - const unseenGroupedNotifications = groupNotifications( - notifications.filter( - (notification) => - !notification.isSeen || - currentlyVisibleUnseenNotificationIds.includes(notification.id) - ), - notificationIdsToHide - ) - setAllNotificationsGroups(allGroupedNotifications) - setUnseenNotificationGroups(unseenGroupedNotifications) - - // We don't want unseenNotificationsGroup to be in the dependencies as we update it here. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [notifications]) - - if (user === undefined) { - return - } - if (user === null) { + if (!user) { + // TODO: return sign in page return } @@ -114,55 +47,18 @@ export default function Notifications() { className={'pb-2 pt-1 '} defaultIndex={0} tabs={[ - { - title: 'New Notifications', - content: ( -
- {unseenNotificationGroups.length === 0 && - "You don't have any new notifications."} - {unseenNotificationGroups.map((notification) => - notification.notifications.length === 1 ? ( - - ) : ( - - ) - )} -
- ), - }, { title: 'All Notifications', content: (
- {allNotificationGroups.length === 0 && - "You don't have any notifications."} - {allNotificationGroups.map((notification) => - notification.notifications.length === 1 ? ( - ( + - ) : ( - - ) - )} -
- ), - }, - { - title: 'Settings', - content: ( -
- + ))}
), }, @@ -173,378 +69,17 @@ export default function Notifications() { ) } -const setNotificationsAsSeen = (notifications: Notification[]) => { - notifications.forEach((notification) => { - if (!notification.isSeen) - updateDoc( - doc(db, `users/${notification.userId}/notifications/`, notification.id), - { - ...notification, - isSeen: true, - viewTime: new Date(), - } - ) - }) - return notifications -} - -function groupNotifications( - notifications: Notification[], - hideNotificationIds: string[] -) { - // Then remove them from the list of notifications to show - notifications = notifications.filter( - (notification) => !hideNotificationIds.includes(notification.id) - ) - - let notificationGroups: NotificationGroup[] = [] - const notificationGroupsByDay = groupBy(notifications, (notification) => - new Date(notification.createdTime).toDateString() - ) - Object.keys(notificationGroupsByDay).forEach((day) => { - // Group notifications by contract: - const groupedNotificationsByContractId = groupBy( - notificationGroupsByDay[day], - (notification) => { - return notification.sourceContractId - } - ) - notificationGroups = notificationGroups.concat( - map(groupedNotificationsByContractId, (notifications, contractId) => { - // Create a notification group for each contract within each day - const notificationGroup: NotificationGroup = { - notifications: groupedNotificationsByContractId[contractId].sort( - (a, b) => { - return b.createdTime - a.createdTime - } - ), - sourceContractId: contractId, - isSeen: groupedNotificationsByContractId[contractId][0].isSeen, - timePeriod: day, - } - return notificationGroup - }) - ) - }) - return notificationGroups -} - -function NotificationGroupItem(props: { - notificationGroup: NotificationGroup - className?: string -}) { - const { notificationGroup, className } = props - const { sourceContractId, notifications } = notificationGroup - const contract = useContract(sourceContractId ?? '') - const numSummaryLines = 3 - const [expanded, setExpanded] = useState(false) - - useEffect(() => { - if (!contract) return - setNotificationsAsSeen(notifications) - }, [contract, notifications]) - - return ( -
setExpanded(!expanded)} - > - {expanded && ( -