2022-06-01 13:11:25 +00:00
|
|
|
import { Tabs } from 'web/components/layout/tabs'
|
|
|
|
import { useUser } from 'web/hooks/use-user'
|
|
|
|
import React, { useEffect, useState } from 'react'
|
2022-06-01 18:26:41 +00:00
|
|
|
import {
|
|
|
|
Notification,
|
|
|
|
notification_reason_types,
|
|
|
|
notification_source_types,
|
2022-06-06 22:15:36 +00:00
|
|
|
notification_source_update_types,
|
2022-06-01 18:26:41 +00:00
|
|
|
} from 'common/notification'
|
2022-06-23 17:55:41 +00:00
|
|
|
import { Avatar, EmptyAvatar } from 'web/components/avatar'
|
2022-06-01 13:11:25 +00:00
|
|
|
import { Row } from 'web/components/layout/row'
|
|
|
|
import { Page } from 'web/components/page'
|
|
|
|
import { Title } from 'web/components/title'
|
|
|
|
import { doc, updateDoc } from 'firebase/firestore'
|
|
|
|
import { db } from 'web/lib/firebase/init'
|
|
|
|
import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-time'
|
|
|
|
import { Answer } from 'common/answer'
|
|
|
|
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'
|
2022-06-06 17:36:59 +00:00
|
|
|
import { notification_subscribe_types, PrivateUser } from 'common/user'
|
2022-06-01 18:26:41 +00:00
|
|
|
import { Contract } from 'common/contract'
|
2022-06-06 17:36:59 +00:00
|
|
|
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 { RelativeTimestamp } from 'web/components/relative-timestamp'
|
|
|
|
import { Linkify } from 'web/components/linkify'
|
|
|
|
import {
|
2022-06-09 16:30:26 +00:00
|
|
|
BinaryOutcomeLabel,
|
|
|
|
CancelLabel,
|
|
|
|
MultiLabel,
|
|
|
|
ProbPercentLabel,
|
2022-06-06 17:36:59 +00:00
|
|
|
} from 'web/components/outcome-label'
|
2022-06-06 22:15:36 +00:00
|
|
|
import {
|
|
|
|
groupNotifications,
|
|
|
|
NotificationGroup,
|
|
|
|
usePreferredGroupedNotifications,
|
|
|
|
} from 'web/hooks/use-notifications'
|
2022-06-06 17:36:59 +00:00
|
|
|
import { getContractFromId } from 'web/lib/firebase/contracts'
|
|
|
|
import { CheckIcon, XIcon } from '@heroicons/react/outline'
|
|
|
|
import toast from 'react-hot-toast'
|
2022-06-13 04:42:41 +00:00
|
|
|
import { formatMoney } from 'common/util/format'
|
2022-06-22 16:35:50 +00:00
|
|
|
import { groupPath } from 'web/lib/firebase/groups'
|
2022-06-06 17:36:59 +00:00
|
|
|
|
2022-06-01 13:11:25 +00:00
|
|
|
export default function Notifications() {
|
|
|
|
const user = useUser()
|
2022-06-06 17:36:59 +00:00
|
|
|
const [unseenNotificationGroups, setUnseenNotificationGroups] = useState<
|
2022-06-08 14:43:24 +00:00
|
|
|
NotificationGroup[] | undefined
|
|
|
|
>(undefined)
|
2022-06-06 22:15:36 +00:00
|
|
|
const allNotificationGroups = usePreferredGroupedNotifications(user?.id, {
|
|
|
|
unseenOnly: false,
|
|
|
|
})
|
2022-06-01 13:11:25 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2022-06-08 14:43:24 +00:00
|
|
|
if (!allNotificationGroups) return
|
2022-06-06 22:15:36 +00:00
|
|
|
// Don't re-add notifications that are visible right now or have been seen already.
|
2022-06-06 17:36:59 +00:00
|
|
|
const currentlyVisibleUnseenNotificationIds = Object.values(
|
2022-06-08 14:43:24 +00:00
|
|
|
unseenNotificationGroups ?? []
|
2022-06-06 17:36:59 +00:00
|
|
|
)
|
|
|
|
.map((n) => n.notifications.map((n) => n.id))
|
|
|
|
.flat()
|
|
|
|
const unseenGroupedNotifications = groupNotifications(
|
2022-06-06 22:15:36 +00:00
|
|
|
allNotificationGroups
|
|
|
|
.map((notification: NotificationGroup) => notification.notifications)
|
|
|
|
.flat()
|
|
|
|
.filter(
|
|
|
|
(notification: Notification) =>
|
|
|
|
!notification.isSeen ||
|
|
|
|
currentlyVisibleUnseenNotificationIds.includes(notification.id)
|
|
|
|
)
|
2022-06-06 17:36:59 +00:00
|
|
|
)
|
|
|
|
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
|
2022-06-06 22:15:36 +00:00
|
|
|
}, [allNotificationGroups])
|
2022-06-06 17:36:59 +00:00
|
|
|
|
|
|
|
if (user === undefined) {
|
|
|
|
return <LoadingIndicator />
|
|
|
|
}
|
|
|
|
if (user === null) {
|
2022-06-01 13:11:25 +00:00
|
|
|
return <Custom404 />
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: use infinite scroll
|
|
|
|
return (
|
|
|
|
<Page>
|
2022-06-01 18:26:41 +00:00
|
|
|
<div className={'p-2 sm:p-4'}>
|
|
|
|
<Title text={'Notifications'} className={'hidden md:block'} />
|
2022-06-01 13:11:25 +00:00
|
|
|
<Tabs
|
|
|
|
className={'pb-2 pt-1 '}
|
|
|
|
defaultIndex={0}
|
|
|
|
tabs={[
|
2022-06-06 17:36:59 +00:00
|
|
|
{
|
|
|
|
title: 'New Notifications',
|
2022-06-08 14:43:24 +00:00
|
|
|
content: unseenNotificationGroups ? (
|
2022-06-06 17:36:59 +00:00
|
|
|
<div className={''}>
|
|
|
|
{unseenNotificationGroups.length === 0 &&
|
|
|
|
"You don't have any new notifications."}
|
|
|
|
{unseenNotificationGroups.map((notification) =>
|
|
|
|
notification.notifications.length === 1 ? (
|
|
|
|
<NotificationItem
|
|
|
|
notification={notification.notifications[0]}
|
|
|
|
key={notification.notifications[0].id}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<NotificationGroupItem
|
|
|
|
notificationGroup={notification}
|
2022-06-06 22:15:36 +00:00
|
|
|
key={
|
|
|
|
notification.sourceContractId +
|
|
|
|
notification.timePeriod
|
|
|
|
}
|
2022-06-06 17:36:59 +00:00
|
|
|
/>
|
|
|
|
)
|
|
|
|
)}
|
|
|
|
</div>
|
2022-06-08 14:43:24 +00:00
|
|
|
) : (
|
|
|
|
<LoadingIndicator />
|
2022-06-06 17:36:59 +00:00
|
|
|
),
|
|
|
|
},
|
2022-06-01 13:11:25 +00:00
|
|
|
{
|
|
|
|
title: 'All Notifications',
|
2022-06-08 14:43:24 +00:00
|
|
|
content: allNotificationGroups ? (
|
2022-06-01 13:11:25 +00:00
|
|
|
<div className={''}>
|
2022-06-06 17:36:59 +00:00
|
|
|
{allNotificationGroups.length === 0 &&
|
2022-06-06 18:01:12 +00:00
|
|
|
"You don't have any notifications. Try changing your settings to see more."}
|
2022-06-06 17:36:59 +00:00
|
|
|
{allNotificationGroups.map((notification) =>
|
|
|
|
notification.notifications.length === 1 ? (
|
|
|
|
<NotificationItem
|
|
|
|
notification={notification.notifications[0]}
|
|
|
|
key={notification.notifications[0].id}
|
|
|
|
/>
|
|
|
|
) : (
|
|
|
|
<NotificationGroupItem
|
|
|
|
notificationGroup={notification}
|
2022-06-06 22:15:36 +00:00
|
|
|
key={
|
|
|
|
notification.sourceContractId +
|
|
|
|
notification.timePeriod
|
|
|
|
}
|
2022-06-01 13:11:25 +00:00
|
|
|
/>
|
2022-06-06 17:36:59 +00:00
|
|
|
)
|
|
|
|
)}
|
|
|
|
</div>
|
2022-06-08 14:43:24 +00:00
|
|
|
) : (
|
|
|
|
<LoadingIndicator />
|
2022-06-06 17:36:59 +00:00
|
|
|
),
|
|
|
|
},
|
|
|
|
{
|
|
|
|
title: 'Settings',
|
|
|
|
content: (
|
|
|
|
<div className={''}>
|
|
|
|
<NotificationSettings />
|
2022-06-01 13:11:25 +00:00
|
|
|
</div>
|
|
|
|
),
|
|
|
|
},
|
|
|
|
]}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</Page>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
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 NotificationGroupItem(props: {
|
|
|
|
notificationGroup: NotificationGroup
|
|
|
|
className?: string
|
|
|
|
}) {
|
|
|
|
const { notificationGroup, className } = props
|
2022-06-13 04:42:41 +00:00
|
|
|
const { sourceContractId, notifications } = notificationGroup
|
2022-06-09 17:39:23 +00:00
|
|
|
const {
|
|
|
|
sourceContractTitle,
|
|
|
|
sourceContractSlug,
|
|
|
|
sourceContractCreatorUsername,
|
|
|
|
} = notifications[0]
|
2022-06-06 17:36:59 +00:00
|
|
|
const numSummaryLines = 3
|
2022-06-09 17:39:23 +00:00
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
const [expanded, setExpanded] = useState(false)
|
2022-06-09 17:39:23 +00:00
|
|
|
const [contract, setContract] = useState<Contract | undefined>(undefined)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (
|
|
|
|
sourceContractTitle &&
|
|
|
|
sourceContractSlug &&
|
|
|
|
sourceContractCreatorUsername
|
|
|
|
)
|
|
|
|
return
|
|
|
|
if (sourceContractId) {
|
|
|
|
getContractFromId(sourceContractId)
|
|
|
|
.then((contract) => {
|
|
|
|
if (contract) setContract(contract)
|
|
|
|
})
|
|
|
|
.catch((e) => console.log(e))
|
|
|
|
}
|
|
|
|
}, [
|
|
|
|
sourceContractCreatorUsername,
|
|
|
|
sourceContractId,
|
|
|
|
sourceContractSlug,
|
|
|
|
sourceContractTitle,
|
|
|
|
])
|
2022-06-06 17:36:59 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
setNotificationsAsSeen(notifications)
|
2022-06-09 17:39:23 +00:00
|
|
|
}, [notifications])
|
2022-06-06 17:36:59 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
className={clsx(
|
|
|
|
'relative cursor-pointer bg-white px-2 pt-6 text-sm',
|
|
|
|
className,
|
|
|
|
!expanded ? 'hover:bg-gray-100' : ''
|
|
|
|
)}
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
|
|
|
>
|
|
|
|
{expanded && (
|
|
|
|
<span
|
|
|
|
className="absolute top-14 left-6 -ml-px h-[calc(100%-5rem)] w-0.5 bg-gray-200"
|
|
|
|
aria-hidden="true"
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
2022-06-23 17:55:41 +00:00
|
|
|
<EmptyAvatar multi />
|
2022-06-06 17:36:59 +00:00
|
|
|
<div className={'flex-1 overflow-hidden pl-2 sm:flex'}>
|
|
|
|
<div
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
|
|
|
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
|
|
|
>
|
2022-06-09 17:39:23 +00:00
|
|
|
{sourceContractTitle || contract ? (
|
2022-06-06 22:36:55 +00:00
|
|
|
<span>
|
|
|
|
{'Activity on '}
|
2022-06-09 17:39:23 +00:00
|
|
|
<a
|
|
|
|
href={
|
|
|
|
sourceContractCreatorUsername
|
|
|
|
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
|
|
|
: `/${contract?.creatorUsername}/${contract?.slug}`
|
|
|
|
}
|
|
|
|
className={
|
|
|
|
'font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
|
|
|
}
|
|
|
|
>
|
|
|
|
{sourceContractTitle || contract?.question}
|
|
|
|
</a>
|
2022-06-06 22:36:55 +00:00
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
'Other activity'
|
|
|
|
)}
|
2022-06-06 17:36:59 +00:00
|
|
|
</div>
|
|
|
|
<RelativeTimestamp time={notifications[0].createdTime} />
|
|
|
|
</div>
|
|
|
|
</Row>
|
|
|
|
<div>
|
|
|
|
<div className={clsx('mt-1 md:text-base', expanded ? 'pl-4' : '')}>
|
|
|
|
{' '}
|
2022-06-09 17:39:23 +00:00
|
|
|
<div className={'line-clamp-4 mt-1 ml-1 gap-1 whitespace-pre-line'}>
|
2022-06-06 17:36:59 +00:00
|
|
|
{!expanded ? (
|
|
|
|
<>
|
2022-06-06 22:15:36 +00:00
|
|
|
{notifications.slice(0, numSummaryLines).map((notification) => {
|
|
|
|
return (
|
|
|
|
<NotificationItem
|
|
|
|
notification={notification}
|
|
|
|
justSummary={true}
|
|
|
|
key={notification.id}
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
})}
|
2022-06-06 17:36:59 +00:00
|
|
|
<div className={'text-sm text-gray-500 hover:underline '}>
|
|
|
|
{notifications.length - numSummaryLines > 0
|
|
|
|
? 'And ' +
|
|
|
|
(notifications.length - numSummaryLines) +
|
|
|
|
' more...'
|
|
|
|
: ''}
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
2022-06-06 22:15:36 +00:00
|
|
|
{notifications.map((notification) => (
|
2022-06-06 17:36:59 +00:00
|
|
|
<NotificationItem
|
|
|
|
notification={notification}
|
|
|
|
key={notification.id}
|
|
|
|
justSummary={false}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className={'mt-6 border-b border-gray-300'} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function NotificationSettings() {
|
|
|
|
const user = useUser()
|
|
|
|
const [notificationSettings, setNotificationSettings] =
|
|
|
|
useState<notification_subscribe_types>('all')
|
|
|
|
const [emailNotificationSettings, setEmailNotificationSettings] =
|
|
|
|
useState<notification_subscribe_types>('all')
|
|
|
|
const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null)
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (user) listenForPrivateUser(user.id, setPrivateUser)
|
|
|
|
}, [user])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!privateUser) return
|
|
|
|
if (privateUser.notificationPreferences) {
|
|
|
|
setNotificationSettings(privateUser.notificationPreferences)
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
privateUser.unsubscribedFromResolutionEmails &&
|
|
|
|
privateUser.unsubscribedFromCommentEmails &&
|
|
|
|
privateUser.unsubscribedFromAnswerEmails
|
|
|
|
) {
|
|
|
|
setEmailNotificationSettings('none')
|
|
|
|
} else if (
|
|
|
|
!privateUser.unsubscribedFromResolutionEmails &&
|
|
|
|
!privateUser.unsubscribedFromCommentEmails &&
|
|
|
|
!privateUser.unsubscribedFromAnswerEmails
|
|
|
|
) {
|
|
|
|
setEmailNotificationSettings('all')
|
|
|
|
} else {
|
|
|
|
setEmailNotificationSettings('less')
|
|
|
|
}
|
|
|
|
}, [privateUser])
|
|
|
|
|
|
|
|
const loading = 'Changing Notifications Settings'
|
|
|
|
const success = 'Notification Settings Changed!'
|
|
|
|
function changeEmailNotifications(newValue: notification_subscribe_types) {
|
|
|
|
if (!privateUser) return
|
|
|
|
if (newValue === 'all') {
|
|
|
|
toast.promise(
|
|
|
|
updatePrivateUser(privateUser.id, {
|
|
|
|
unsubscribedFromResolutionEmails: false,
|
|
|
|
unsubscribedFromCommentEmails: false,
|
|
|
|
unsubscribedFromAnswerEmails: false,
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
loading,
|
|
|
|
success,
|
|
|
|
error: (err) => `${err.message}`,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else if (newValue === 'less') {
|
|
|
|
toast.promise(
|
|
|
|
updatePrivateUser(privateUser.id, {
|
|
|
|
unsubscribedFromResolutionEmails: false,
|
|
|
|
unsubscribedFromCommentEmails: true,
|
|
|
|
unsubscribedFromAnswerEmails: true,
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
loading,
|
|
|
|
success,
|
|
|
|
error: (err) => `${err.message}`,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
} else if (newValue === 'none') {
|
|
|
|
toast.promise(
|
|
|
|
updatePrivateUser(privateUser.id, {
|
|
|
|
unsubscribedFromResolutionEmails: true,
|
|
|
|
unsubscribedFromCommentEmails: true,
|
|
|
|
unsubscribedFromAnswerEmails: true,
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
loading,
|
|
|
|
success,
|
|
|
|
error: (err) => `${err.message}`,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function changeInAppNotificationSettings(
|
|
|
|
newValue: notification_subscribe_types
|
|
|
|
) {
|
|
|
|
if (!privateUser) return
|
|
|
|
toast.promise(
|
|
|
|
updatePrivateUser(privateUser.id, {
|
|
|
|
notificationPreferences: newValue,
|
|
|
|
}),
|
|
|
|
{
|
|
|
|
loading,
|
|
|
|
success,
|
|
|
|
error: (err) => `${err.message}`,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (privateUser && privateUser.notificationPreferences)
|
|
|
|
setNotificationSettings(privateUser.notificationPreferences)
|
|
|
|
else setNotificationSettings('all')
|
|
|
|
}, [privateUser])
|
|
|
|
|
|
|
|
if (!privateUser) {
|
|
|
|
return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
|
|
|
|
}
|
|
|
|
|
|
|
|
function NotificationSettingLine(props: {
|
|
|
|
label: string
|
|
|
|
highlight: boolean
|
|
|
|
}) {
|
|
|
|
const { label, highlight } = props
|
|
|
|
return (
|
|
|
|
<Row className={clsx('my-1 text-gray-300', highlight && '!text-black')}>
|
|
|
|
{highlight ? <CheckIcon height={20} /> : <XIcon height={20} />}
|
|
|
|
{label}
|
|
|
|
</Row>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={'p-2'}>
|
|
|
|
<div>In App Notifications</div>
|
|
|
|
<ChoicesToggleGroup
|
|
|
|
currentChoice={notificationSettings}
|
|
|
|
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
|
|
|
|
setChoice={(choice) =>
|
|
|
|
changeInAppNotificationSettings(
|
|
|
|
choice as notification_subscribe_types
|
|
|
|
)
|
|
|
|
}
|
|
|
|
className={'col-span-4 p-2'}
|
|
|
|
toggleClassName={'w-24'}
|
|
|
|
/>
|
2022-06-08 17:24:07 +00:00
|
|
|
<div className={'mt-4 text-sm'}>
|
|
|
|
<div>
|
|
|
|
<div className={''}>
|
|
|
|
You will receive notifications for:
|
|
|
|
<NotificationSettingLine
|
|
|
|
label={"Resolution of questions you've interacted with"}
|
|
|
|
highlight={notificationSettings !== 'none'}
|
|
|
|
/>
|
|
|
|
<NotificationSettingLine
|
|
|
|
highlight={notificationSettings !== 'none'}
|
|
|
|
label={'Activity on your own questions, comments, & answers'}
|
|
|
|
/>
|
|
|
|
<NotificationSettingLine
|
|
|
|
highlight={notificationSettings !== 'none'}
|
|
|
|
label={"Activity on questions you're betting on"}
|
|
|
|
/>
|
|
|
|
<NotificationSettingLine
|
|
|
|
label={"Activity on questions you've ever bet or commented on"}
|
|
|
|
highlight={notificationSettings === 'all'}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-06-06 17:36:59 +00:00
|
|
|
<div className={'mt-4'}>Email Notifications</div>
|
|
|
|
<ChoicesToggleGroup
|
|
|
|
currentChoice={emailNotificationSettings}
|
|
|
|
choicesMap={{ All: 'all', Less: 'less', None: 'none' }}
|
|
|
|
setChoice={(choice) =>
|
|
|
|
changeEmailNotifications(choice as notification_subscribe_types)
|
|
|
|
}
|
|
|
|
className={'col-span-4 p-2'}
|
|
|
|
toggleClassName={'w-24'}
|
|
|
|
/>
|
2022-06-08 17:24:07 +00:00
|
|
|
<div className={'mt-4 text-sm'}>
|
|
|
|
<div>
|
|
|
|
You will receive emails for:
|
|
|
|
<NotificationSettingLine
|
|
|
|
label={"Resolution of questions you're betting on"}
|
|
|
|
highlight={emailNotificationSettings !== 'none'}
|
|
|
|
/>
|
|
|
|
<NotificationSettingLine
|
|
|
|
label={'Closure of your questions'}
|
|
|
|
highlight={emailNotificationSettings !== 'none'}
|
|
|
|
/>
|
|
|
|
<NotificationSettingLine
|
|
|
|
label={'Activity on your questions'}
|
|
|
|
highlight={emailNotificationSettings === 'all'}
|
|
|
|
/>
|
|
|
|
<NotificationSettingLine
|
|
|
|
label={"Activity on questions you've answered or commented on"}
|
|
|
|
highlight={emailNotificationSettings === 'all'}
|
|
|
|
/>
|
|
|
|
</div>
|
2022-06-06 17:36:59 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-06-06 22:15:36 +00:00
|
|
|
function isNotificationAboutContractResolution(
|
|
|
|
sourceType: notification_source_types | undefined,
|
|
|
|
sourceUpdateType: notification_source_update_types | undefined,
|
|
|
|
contract: Contract | null | undefined
|
2022-06-06 17:36:59 +00:00
|
|
|
) {
|
2022-06-06 22:15:36 +00:00
|
|
|
return (
|
2022-06-08 14:43:24 +00:00
|
|
|
(sourceType === 'contract' && sourceUpdateType === 'resolved') ||
|
|
|
|
(sourceType === 'contract' && !sourceUpdateType && contract?.resolution)
|
2022-06-06 22:15:36 +00:00
|
|
|
)
|
2022-06-06 17:36:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function NotificationItem(props: {
|
2022-06-01 13:11:25 +00:00
|
|
|
notification: Notification
|
2022-06-06 17:36:59 +00:00
|
|
|
justSummary?: boolean
|
2022-06-01 13:11:25 +00:00
|
|
|
}) {
|
2022-06-06 17:36:59 +00:00
|
|
|
const { notification, justSummary } = props
|
2022-06-01 13:11:25 +00:00
|
|
|
const {
|
|
|
|
sourceType,
|
|
|
|
sourceContractId,
|
|
|
|
sourceId,
|
|
|
|
sourceUserName,
|
|
|
|
sourceUserAvatarUrl,
|
2022-06-06 22:15:36 +00:00
|
|
|
sourceUpdateType,
|
2022-06-01 13:11:25 +00:00
|
|
|
reasonText,
|
2022-06-01 18:26:41 +00:00
|
|
|
reason,
|
2022-06-01 13:11:25 +00:00
|
|
|
sourceUserUsername,
|
|
|
|
createdTime,
|
2022-06-08 14:43:24 +00:00
|
|
|
sourceText,
|
|
|
|
sourceContractTitle,
|
|
|
|
sourceContractCreatorUsername,
|
|
|
|
sourceContractSlug,
|
2022-06-22 16:35:50 +00:00
|
|
|
sourceSlug,
|
|
|
|
sourceTitle,
|
2022-06-01 13:11:25 +00:00
|
|
|
} = notification
|
2022-06-09 17:39:23 +00:00
|
|
|
|
|
|
|
const [defaultNotificationText, setDefaultNotificationText] =
|
|
|
|
useState<string>('')
|
2022-06-06 17:36:59 +00:00
|
|
|
const [contract, setContract] = useState<Contract | null>(null)
|
2022-06-06 22:15:36 +00:00
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
useEffect(() => {
|
2022-06-08 14:43:24 +00:00
|
|
|
if (
|
|
|
|
!sourceContractId ||
|
|
|
|
(sourceContractSlug && sourceContractCreatorUsername)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
getContractFromId(sourceContractId)
|
|
|
|
.then((contract) => {
|
|
|
|
if (contract) setContract(contract)
|
|
|
|
})
|
|
|
|
.catch((e) => console.log(e))
|
|
|
|
}, [
|
|
|
|
sourceContractCreatorUsername,
|
|
|
|
sourceContractId,
|
|
|
|
sourceContractSlug,
|
|
|
|
sourceContractTitle,
|
|
|
|
])
|
2022-06-01 13:11:25 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2022-06-09 17:39:23 +00:00
|
|
|
if (sourceText) {
|
|
|
|
setDefaultNotificationText(sourceText)
|
2022-06-08 14:43:24 +00:00
|
|
|
} else if (!contract || !sourceContractId || !sourceId) return
|
|
|
|
else if (
|
2022-06-06 22:15:36 +00:00
|
|
|
sourceType === 'answer' ||
|
|
|
|
sourceType === 'comment' ||
|
|
|
|
sourceType === 'contract'
|
|
|
|
) {
|
2022-06-08 14:43:24 +00:00
|
|
|
try {
|
2022-06-10 22:48:28 +00:00
|
|
|
parseOldStyleNotificationText(
|
2022-06-08 14:43:24 +00:00
|
|
|
sourceId,
|
|
|
|
sourceContractId,
|
|
|
|
sourceType,
|
|
|
|
sourceUpdateType,
|
2022-06-09 17:39:23 +00:00
|
|
|
setDefaultNotificationText,
|
2022-06-08 14:43:24 +00:00
|
|
|
contract
|
|
|
|
)
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
}
|
2022-06-06 17:36:59 +00:00
|
|
|
} else if (reasonText) {
|
|
|
|
// Handle arbitrary notifications with reason text here.
|
2022-06-09 17:39:23 +00:00
|
|
|
setDefaultNotificationText(reasonText)
|
2022-06-01 13:11:25 +00:00
|
|
|
}
|
2022-06-06 22:15:36 +00:00
|
|
|
}, [
|
|
|
|
contract,
|
|
|
|
reasonText,
|
|
|
|
sourceContractId,
|
|
|
|
sourceId,
|
2022-06-08 14:43:24 +00:00
|
|
|
sourceText,
|
2022-06-06 22:15:36 +00:00
|
|
|
sourceType,
|
|
|
|
sourceUpdateType,
|
|
|
|
])
|
2022-06-01 13:11:25 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2022-06-06 17:36:59 +00:00
|
|
|
setNotificationsAsSeen([notification])
|
|
|
|
}, [notification])
|
2022-06-01 13:11:25 +00:00
|
|
|
|
2022-06-06 22:30:31 +00:00
|
|
|
function getSourceUrl() {
|
|
|
|
if (sourceType === 'follow') return `/${sourceUserUsername}`
|
2022-06-22 16:35:50 +00:00
|
|
|
if (sourceType === 'group' && sourceSlug) return `${groupPath(sourceSlug)}`
|
2022-06-08 14:43:24 +00:00
|
|
|
if (sourceContractCreatorUsername && sourceContractSlug)
|
|
|
|
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
|
|
|
|
sourceId ?? ''
|
|
|
|
)}`
|
2022-06-01 13:11:25 +00:00
|
|
|
if (!contract) return ''
|
|
|
|
return `/${contract.creatorUsername}/${
|
|
|
|
contract.slug
|
|
|
|
}#${getSourceIdForLinkComponent(sourceId ?? '')}`
|
|
|
|
}
|
|
|
|
|
|
|
|
function getSourceIdForLinkComponent(sourceId: string) {
|
|
|
|
switch (sourceType) {
|
|
|
|
case 'answer':
|
|
|
|
return `answer-${sourceId}`
|
|
|
|
case 'comment':
|
|
|
|
return sourceId
|
|
|
|
case 'contract':
|
|
|
|
return ''
|
|
|
|
default:
|
|
|
|
return sourceId
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-10 22:48:28 +00:00
|
|
|
async function parseOldStyleNotificationText(
|
2022-06-06 22:15:36 +00:00
|
|
|
sourceId: string,
|
|
|
|
sourceContractId: string,
|
|
|
|
sourceType: 'answer' | 'comment' | 'contract',
|
|
|
|
sourceUpdateType: notification_source_update_types | undefined,
|
|
|
|
setText: (text: string) => void,
|
2022-06-08 14:43:24 +00:00
|
|
|
contract: Contract
|
2022-06-06 22:15:36 +00:00
|
|
|
) {
|
2022-06-08 14:43:24 +00:00
|
|
|
if (sourceType === 'contract') {
|
2022-06-06 22:15:36 +00:00
|
|
|
if (
|
|
|
|
isNotificationAboutContractResolution(
|
|
|
|
sourceType,
|
|
|
|
sourceUpdateType,
|
|
|
|
contract
|
|
|
|
) &&
|
2022-06-08 14:43:24 +00:00
|
|
|
contract.resolution
|
2022-06-06 22:15:36 +00:00
|
|
|
)
|
|
|
|
setText(contract.resolution)
|
|
|
|
else setText(contract.question)
|
|
|
|
} else if (sourceType === 'answer') {
|
|
|
|
const answer = await getValue<Answer>(
|
|
|
|
doc(db, `contracts/${sourceContractId}/answers/`, sourceId)
|
|
|
|
)
|
|
|
|
setText(answer?.text ?? '')
|
|
|
|
} else {
|
|
|
|
const comment = await getValue<Comment>(
|
|
|
|
doc(db, `contracts/${sourceContractId}/comments/`, sourceId)
|
|
|
|
)
|
|
|
|
setText(comment?.text ?? '')
|
|
|
|
}
|
2022-06-06 17:36:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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'}>
|
|
|
|
<UserLink
|
|
|
|
name={sourceUserName || ''}
|
|
|
|
username={sourceUserUsername || ''}
|
|
|
|
className={'mr-0 flex-shrink-0'}
|
|
|
|
/>
|
|
|
|
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
2022-06-08 14:43:24 +00:00
|
|
|
<span className={'flex-shrink-0'}>
|
|
|
|
{sourceType &&
|
|
|
|
reason &&
|
|
|
|
getReasonForShowingNotification(
|
|
|
|
sourceType,
|
|
|
|
reason,
|
|
|
|
sourceUpdateType,
|
|
|
|
contract,
|
|
|
|
true
|
|
|
|
).replace(' on', '')}
|
|
|
|
</span>
|
2022-06-06 17:36:59 +00:00
|
|
|
<div className={'ml-1 text-black'}>
|
2022-06-08 14:43:24 +00:00
|
|
|
<NotificationTextLabel
|
|
|
|
contract={contract}
|
2022-06-09 17:39:23 +00:00
|
|
|
defaultText={defaultNotificationText}
|
2022-06-08 14:43:24 +00:00
|
|
|
className={'line-clamp-1'}
|
|
|
|
notification={notification}
|
2022-06-09 17:39:23 +00:00
|
|
|
justSummary={true}
|
2022-06-08 14:43:24 +00:00
|
|
|
/>
|
2022-06-06 17:36:59 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</Row>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-06-01 13:11:25 +00:00
|
|
|
return (
|
2022-06-06 17:36:59 +00:00
|
|
|
<div className={'bg-white px-2 pt-6 text-sm sm:px-4'}>
|
2022-06-06 22:30:31 +00:00
|
|
|
<a href={getSourceUrl()}>
|
2022-06-06 22:15:36 +00:00
|
|
|
<Row className={'items-center text-gray-500 sm:justify-start'}>
|
|
|
|
<Avatar
|
|
|
|
avatarUrl={sourceUserAvatarUrl}
|
|
|
|
size={'sm'}
|
|
|
|
className={'mr-2'}
|
|
|
|
username={sourceUserName}
|
|
|
|
/>
|
|
|
|
<div className={'flex-1 overflow-hidden sm:flex'}>
|
|
|
|
<div
|
|
|
|
className={
|
|
|
|
'flex max-w-xl shrink overflow-hidden text-ellipsis pl-1 sm:pl-0'
|
|
|
|
}
|
2022-06-01 18:26:41 +00:00
|
|
|
>
|
2022-06-06 22:15:36 +00:00
|
|
|
<UserLink
|
|
|
|
name={sourceUserName || ''}
|
|
|
|
username={sourceUserUsername || ''}
|
|
|
|
className={'mr-0 flex-shrink-0'}
|
|
|
|
/>
|
|
|
|
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
|
|
|
|
{sourceType && reason && (
|
|
|
|
<div className={'inline truncate'}>
|
|
|
|
{getReasonForShowingNotification(
|
|
|
|
sourceType,
|
|
|
|
reason,
|
|
|
|
sourceUpdateType,
|
|
|
|
contract
|
|
|
|
)}
|
2022-06-09 17:39:23 +00:00
|
|
|
<a
|
|
|
|
href={
|
|
|
|
sourceContractCreatorUsername
|
|
|
|
? `/${sourceContractCreatorUsername}/${sourceContractSlug}`
|
2022-06-22 16:35:50 +00:00
|
|
|
: sourceType === 'group' && sourceSlug
|
|
|
|
? `${groupPath(sourceSlug)}`
|
2022-06-09 17:39:23 +00:00
|
|
|
: `/${contract?.creatorUsername}/${contract?.slug}`
|
|
|
|
}
|
|
|
|
className={
|
|
|
|
'ml-1 font-bold hover:underline hover:decoration-indigo-400 hover:decoration-2'
|
|
|
|
}
|
|
|
|
>
|
2022-06-22 16:35:50 +00:00
|
|
|
{contract?.question || sourceContractTitle || sourceTitle}
|
2022-06-09 17:39:23 +00:00
|
|
|
</a>
|
2022-06-06 22:15:36 +00:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
2022-06-09 17:39:23 +00:00
|
|
|
{sourceId && sourceContractSlug && sourceContractCreatorUsername ? (
|
2022-06-06 22:15:36 +00:00
|
|
|
<CopyLinkDateTimeComponent
|
2022-06-22 16:35:50 +00:00
|
|
|
prefix={sourceContractCreatorUsername}
|
|
|
|
slug={sourceContractSlug}
|
2022-06-06 22:15:36 +00:00
|
|
|
createdTime={createdTime}
|
|
|
|
elementId={getSourceIdForLinkComponent(sourceId)}
|
|
|
|
className={'-mx-1 inline-flex sm:inline-block'}
|
|
|
|
/>
|
2022-06-09 17:39:23 +00:00
|
|
|
) : (
|
|
|
|
<RelativeTimestamp time={createdTime} />
|
2022-06-06 22:15:36 +00:00
|
|
|
)}
|
2022-06-01 18:26:41 +00:00
|
|
|
</div>
|
2022-06-06 22:15:36 +00:00
|
|
|
</Row>
|
2022-06-09 17:39:23 +00:00
|
|
|
<div className={'mt-1 ml-1 md:text-base'}>
|
2022-06-08 14:43:24 +00:00
|
|
|
<NotificationTextLabel
|
|
|
|
contract={contract}
|
2022-06-09 17:39:23 +00:00
|
|
|
defaultText={defaultNotificationText}
|
2022-06-08 14:43:24 +00:00
|
|
|
notification={notification}
|
|
|
|
/>
|
2022-06-01 13:11:25 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className={'mt-6 border-b border-gray-300'} />
|
|
|
|
</a>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
2022-06-01 18:26:41 +00:00
|
|
|
|
2022-06-06 17:36:59 +00:00
|
|
|
function NotificationTextLabel(props: {
|
2022-06-06 22:15:36 +00:00
|
|
|
defaultText: string
|
2022-06-08 14:43:24 +00:00
|
|
|
contract?: Contract | null
|
|
|
|
notification: Notification
|
2022-06-06 17:36:59 +00:00
|
|
|
className?: string
|
2022-06-09 17:39:23 +00:00
|
|
|
justSummary?: boolean
|
2022-06-06 17:36:59 +00:00
|
|
|
}) {
|
2022-06-09 17:39:23 +00:00
|
|
|
const { contract, className, defaultText, notification, justSummary } = props
|
2022-06-08 14:43:24 +00:00
|
|
|
const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } =
|
|
|
|
notification
|
2022-06-10 22:48:28 +00:00
|
|
|
if (sourceType === 'contract') {
|
|
|
|
if (justSummary)
|
|
|
|
return <span>{contract?.question || sourceContractTitle}</span>
|
|
|
|
if (!sourceText) return <div />
|
|
|
|
// Resolved contracts
|
|
|
|
if (
|
|
|
|
isNotificationAboutContractResolution(
|
|
|
|
sourceType,
|
|
|
|
sourceUpdateType,
|
|
|
|
contract
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
{
|
|
|
|
if (sourceText === 'YES' || sourceText == 'NO') {
|
|
|
|
return <BinaryOutcomeLabel outcome={sourceText as any} />
|
|
|
|
}
|
|
|
|
if (sourceText.includes('%'))
|
|
|
|
return (
|
|
|
|
<ProbPercentLabel prob={parseFloat(sourceText.replace('%', ''))} />
|
|
|
|
)
|
|
|
|
if (sourceText === 'CANCEL') return <CancelLabel />
|
|
|
|
if (sourceText === 'MKT' || sourceText === 'PROB') return <MultiLabel />
|
2022-06-09 16:30:26 +00:00
|
|
|
}
|
2022-06-10 22:48:28 +00:00
|
|
|
}
|
|
|
|
// Close date will be a number - it looks better without it
|
|
|
|
if (sourceUpdateType === 'closed') {
|
|
|
|
return <div />
|
|
|
|
}
|
|
|
|
// Updated contracts
|
|
|
|
// Description will be in default text
|
|
|
|
if (parseInt(sourceText) > 0) {
|
2022-06-06 17:36:59 +00:00
|
|
|
return (
|
2022-06-10 22:48:28 +00:00
|
|
|
<span>
|
|
|
|
Updated close time: {new Date(parseInt(sourceText)).toLocaleString()}
|
|
|
|
</span>
|
2022-06-06 17:36:59 +00:00
|
|
|
)
|
2022-06-10 22:48:28 +00:00
|
|
|
}
|
|
|
|
} else if (sourceType === 'liquidity' && sourceText) {
|
2022-06-06 17:36:59 +00:00
|
|
|
return (
|
2022-06-10 22:48:28 +00:00
|
|
|
<span className="text-blue-400">{formatMoney(parseInt(sourceText))}</span>
|
2022-06-06 17:36:59 +00:00
|
|
|
)
|
|
|
|
}
|
2022-06-10 22:48:28 +00:00
|
|
|
// return default text
|
|
|
|
return (
|
|
|
|
<div className={className ? className : 'line-clamp-4 whitespace-pre-line'}>
|
|
|
|
<Linkify text={defaultText} />
|
|
|
|
</div>
|
|
|
|
)
|
2022-06-06 17:36:59 +00:00
|
|
|
}
|
|
|
|
|
2022-06-06 22:15:36 +00:00
|
|
|
function getReasonForShowingNotification(
|
2022-06-01 18:26:41 +00:00
|
|
|
source: notification_source_types,
|
|
|
|
reason: notification_reason_types,
|
2022-06-06 22:15:36 +00:00
|
|
|
sourceUpdateType: notification_source_update_types | undefined,
|
2022-06-06 17:36:59 +00:00
|
|
|
contract: Contract | undefined | null,
|
|
|
|
simple?: boolean
|
2022-06-01 18:26:41 +00:00
|
|
|
) {
|
2022-06-06 22:15:36 +00:00
|
|
|
let reasonText: string
|
2022-06-01 18:26:41 +00:00
|
|
|
switch (source) {
|
|
|
|
case 'comment':
|
2022-06-06 17:36:59 +00:00
|
|
|
if (reason === 'reply_to_users_answer')
|
|
|
|
reasonText = !simple ? 'replied to your answer on' : 'replied'
|
|
|
|
else if (reason === 'tagged_user')
|
|
|
|
reasonText = !simple ? 'tagged you in a comment on' : 'tagged you'
|
|
|
|
else if (reason === 'reply_to_users_comment')
|
|
|
|
reasonText = !simple ? 'replied to your comment on' : 'replied'
|
|
|
|
else if (reason === 'on_users_contract')
|
|
|
|
reasonText = !simple ? `commented on your question` : 'commented'
|
|
|
|
else if (reason === 'on_contract_with_users_comment')
|
|
|
|
reasonText = `commented on`
|
|
|
|
else if (reason === 'on_contract_with_users_answer')
|
|
|
|
reasonText = `commented on`
|
|
|
|
else if (reason === 'on_contract_with_users_shares_in')
|
|
|
|
reasonText = `commented`
|
|
|
|
else reasonText = `commented on`
|
|
|
|
break
|
2022-06-01 18:26:41 +00:00
|
|
|
case 'contract':
|
2022-06-08 14:43:24 +00:00
|
|
|
if (reason === 'you_follow_user') reasonText = 'created a new question'
|
|
|
|
else if (
|
2022-06-06 22:15:36 +00:00
|
|
|
isNotificationAboutContractResolution(
|
|
|
|
source,
|
|
|
|
sourceUpdateType,
|
|
|
|
contract
|
|
|
|
)
|
|
|
|
)
|
|
|
|
reasonText = `resolved`
|
2022-06-10 22:48:28 +00:00
|
|
|
else if (sourceUpdateType === 'closed')
|
|
|
|
reasonText = `please resolve your question`
|
2022-06-06 17:36:59 +00:00
|
|
|
else reasonText = `updated`
|
|
|
|
break
|
2022-06-01 18:26:41 +00:00
|
|
|
case 'answer':
|
2022-06-06 17:36:59 +00:00
|
|
|
if (reason === 'on_users_contract') reasonText = `answered your question `
|
2022-06-08 14:43:24 +00:00
|
|
|
else if (reason === 'on_contract_with_users_comment')
|
|
|
|
reasonText = `answered`
|
2022-06-06 17:36:59 +00:00
|
|
|
else if (reason === 'on_contract_with_users_answer')
|
|
|
|
reasonText = `answered`
|
|
|
|
else if (reason === 'on_contract_with_users_shares_in')
|
|
|
|
reasonText = `answered`
|
|
|
|
else reasonText = `answered`
|
|
|
|
break
|
|
|
|
case 'follow':
|
|
|
|
reasonText = 'followed you'
|
|
|
|
break
|
2022-06-10 22:48:28 +00:00
|
|
|
case 'liquidity':
|
|
|
|
reasonText = 'added liquidity to your question'
|
|
|
|
break
|
2022-06-22 16:35:50 +00:00
|
|
|
case 'group':
|
|
|
|
reasonText = 'added you to the group'
|
|
|
|
break
|
2022-06-01 18:26:41 +00:00
|
|
|
default:
|
2022-06-06 17:36:59 +00:00
|
|
|
reasonText = ''
|
2022-06-01 18:26:41 +00:00
|
|
|
}
|
2022-06-06 17:36:59 +00:00
|
|
|
return reasonText
|
2022-06-01 18:26:41 +00:00
|
|
|
}
|