manifold/web/pages/notifications.tsx
Ian Philips b5057f4028
Notifications ux fixes - wip (#383)
* Notifications generating on comment,answer,contract update

* Notifications MVP

* Submitted an answer => answered

* Listen for unseen notifications

* Fix userlink formatting, move page

* Fix links

* Remove redundant code

* Cleanup

* Cleanup

* Refactor name

* Comments

* Cleanup & update notif only after data retrieval

* Find initial new notifs on user change

* Enforce auth rules in db

* eslint update

* Code review changes

* Refactor reason

* Add todos

* Show question in notifiation title

* Allow larger width on md

* Condense on mobile

* Decrease padding, hide title on mobile

* Line clamp notifications

* Shrink text
2022-06-01 11:31:46 -06:00

222 lines
6.3 KiB
TypeScript

import { Tabs } from 'web/components/layout/tabs'
import { useUser } from 'web/hooks/use-user'
import React, { useEffect, useState } from 'react'
import {
Notification,
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'
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'
import { User } from 'common/user'
import { useContract } from 'web/hooks/use-contract'
import { Contract } from 'common/contract'
export default function Notifications() {
const user = useUser()
const [notifications, setNotifications] = useState<
Notification[] | undefined
>()
useEffect(() => {
if (user) return listenForNotifications(user.id, setNotifications)
}, [user])
if (!user) {
// TODO: return sign in page
return <Custom404 />
}
// TODO: use infinite scroll
return (
<Page>
<div className={'p-2 sm:p-4'}>
<Title text={'Notifications'} className={'hidden sm:block'} />
<Tabs
className={'pb-2 pt-1 '}
defaultIndex={0}
tabs={[
{
title: 'All Notifications',
content: (
<div className={''}>
{notifications &&
notifications.map((notification) => (
<Notification
currentUser={user}
notification={notification}
key={notification.id}
/>
))}
</div>
),
},
]}
/>
</div>
</Page>
)
}
function Notification(props: {
currentUser: User
notification: Notification
}) {
const { notification, currentUser } = props
const {
sourceType,
sourceContractId,
sourceId,
userId,
id,
sourceUserName,
sourceUserAvatarUrl,
reasonText,
reason,
sourceUserUsername,
createdTime,
} = notification
const [subText, setSubText] = useState<string>('')
const contract = useContract(sourceContractId ?? '')
useEffect(() => {
if (!contract) return
if (sourceType === 'contract') {
setSubText(contract.question)
}
}, [contract, sourceType])
useEffect(() => {
if (!sourceContractId || !sourceId) return
if (sourceType === 'answer') {
getValue<Answer>(
doc(db, `contracts/${sourceContractId}/answers/`, sourceId)
).then((answer) => {
setSubText(answer?.text || '')
})
} else if (sourceType === 'comment') {
getValue<Comment>(
doc(db, `contracts/${sourceContractId}/comments/`, sourceId)
).then((comment) => {
setSubText(comment?.text || '')
})
}
}, [sourceContractId, sourceId, sourceType])
useEffect(() => {
if (!contract || !notification || notification.isSeen) return
updateDoc(doc(db, `users/${currentUser.id}/notifications/`, id), {
...notification,
isSeen: true,
viewTime: new Date(),
})
}, [notification, contract, currentUser, id, userId])
function getSourceUrl(sourceId?: string) {
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
}
}
return (
<div className={'bg-white px-1 pt-6 text-sm sm:px-4'}>
<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-sm shrink overflow-hidden text-ellipsis sm:max-w-md'
}
>
<UserLink
name={sourceUserName || ''}
username={sourceUserUsername || ''}
className={'mr-0 flex-shrink-0'}
/>
<a
href={getSourceUrl(sourceId)}
className={'inline-flex overflow-hidden text-ellipsis pl-1'}
>
{sourceType && reason ? (
<div className={'inline truncate'}>
{getReasonTextFromReason(sourceType, reason, contract)}
</div>
) : (
reasonText
)}
</a>
</div>
{contract && sourceId && (
<CopyLinkDateTimeComponent
contract={contract}
createdTime={createdTime}
elementId={getSourceIdForLinkComponent(sourceId)}
className={'-mx-1 inline-flex sm:inline-block'}
/>
)}
</div>
</Row>
<a href={getSourceUrl(sourceId)}>
<div className={'mt-1 md:text-base'}>
{' '}
{contract && subText === contract.question ? (
<div className={'text-indigo-700 hover:underline'}>{subText}</div>
) : (
<div className={'line-clamp-4 whitespace-pre-line'}>{subText}</div>
)}
</div>
<div className={'mt-6 border-b border-gray-300'} />
</a>
</div>
)
}
function getReasonTextFromReason(
source: notification_source_types,
reason: notification_reason_types,
contract: Contract | undefined
) {
switch (source) {
case 'comment':
return `commented on ${contract?.question}`
case 'contract':
return `${reason} ${contract?.question}`
case 'answer':
return `answered ${contract?.question}`
default:
return ''
}
}