Speed up notification loading by prepopulating relevant info (#453)

* Populate notification with relevant info

* eslint

* Remove duplicated code

* Unused ?

* Add new q notification, other small fixes
This commit is contained in:
Ian Philips 2022-06-08 08:43:24 -06:00 committed by GitHub
parent 7e37fc776c
commit 936cabe353
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 198 additions and 92 deletions

View File

@ -14,6 +14,10 @@ export type Notification = {
sourceUserName?: string sourceUserName?: string
sourceUserUsername?: string sourceUserUsername?: string
sourceUserAvatarUrl?: string sourceUserAvatarUrl?: string
sourceText?: string
sourceContractTitle?: string
sourceContractCreatorUsername?: string
sourceContractSlug?: string
} }
export type notification_source_types = export type notification_source_types =
| 'contract' | 'contract'
@ -42,3 +46,4 @@ export type notification_reason_types =
| 'reply_to_users_answer' | 'reply_to_users_answer'
| 'reply_to_users_comment' | 'reply_to_users_comment'
| 'on_new_follow' | 'on_new_follow'
| 'you_follow_user'

View File

@ -26,10 +26,10 @@ export const createNotification = async (
sourceUpdateType: notification_source_update_types, sourceUpdateType: notification_source_update_types,
sourceUser: User, sourceUser: User,
idempotencyKey: string, idempotencyKey: string,
sourceText: string,
sourceContract?: Contract, sourceContract?: Contract,
relatedSourceType?: notification_source_types, relatedSourceType?: notification_source_types,
relatedUserId?: string, relatedUserId?: string
sourceText?: string
) => { ) => {
const shouldGetNotification = ( const shouldGetNotification = (
userId: string, userId: string,
@ -62,12 +62,37 @@ export const createNotification = async (
sourceUserName: sourceUser.name, sourceUserName: sourceUser.name,
sourceUserUsername: sourceUser.username, sourceUserUsername: sourceUser.username,
sourceUserAvatarUrl: sourceUser.avatarUrl, sourceUserAvatarUrl: sourceUser.avatarUrl,
sourceText,
sourceContractTitle: sourceContract?.question,
sourceContractCreatorUsername: sourceContract?.creatorUsername,
sourceContractSlug: sourceContract?.slug,
} }
await notificationRef.set(removeUndefinedProps(notification)) await notificationRef.set(removeUndefinedProps(notification))
}) })
) )
} }
const notifyUsersFollowers = async (
userToReasonTexts: user_to_reason_texts
) => {
const followers = await firestore
.collectionGroup('follows')
.where('userId', '==', sourceUser.id)
.get()
followers.docs.forEach((doc) => {
const followerUserId = doc.ref.parent.parent?.id
if (
followerUserId &&
shouldGetNotification(followerUserId, userToReasonTexts)
) {
userToReasonTexts[followerUserId] = {
reason: 'you_follow_user',
}
}
})
}
const notifyRepliedUsers = async ( const notifyRepliedUsers = async (
userToReasonTexts: user_to_reason_texts userToReasonTexts: user_to_reason_texts
) => { ) => {
@ -200,7 +225,8 @@ export const createNotification = async (
sourceContract && sourceContract &&
(sourceType === 'comment' || (sourceType === 'comment' ||
sourceType === 'answer' || sourceType === 'answer' ||
sourceType === 'contract') (sourceType === 'contract' &&
(sourceUpdateType === 'updated' || sourceUpdateType === 'resolved')))
) { ) {
if (sourceType === 'comment') { if (sourceType === 'comment') {
await notifyRepliedUsers(userToReasonTexts) await notifyRepliedUsers(userToReasonTexts)
@ -212,6 +238,8 @@ export const createNotification = async (
await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract) await notifyOtherCommentersOnContract(userToReasonTexts, sourceContract)
} else if (sourceType === 'follow' && relatedUserId) { } else if (sourceType === 'follow' && relatedUserId) {
await notifyFollowedUser(userToReasonTexts, relatedUserId) await notifyFollowedUser(userToReasonTexts, relatedUserId)
} else if (sourceType === 'contract' && sourceUpdateType === 'created') {
await notifyUsersFollowers(userToReasonTexts)
} }
return userToReasonTexts return userToReasonTexts
} }

View File

@ -26,6 +26,7 @@ export * from './market-close-emails'
export * from './add-liquidity' export * from './add-liquidity'
export * from './on-create-answer' export * from './on-create-answer'
export * from './on-update-contract' export * from './on-update-contract'
export * from './on-create-contract'
export * from './on-follow-user' export * from './on-follow-user'
// v2 // v2

View File

@ -27,6 +27,7 @@ export const onCreateAnswer = functions.firestore
'created', 'created',
answerCreator, answerCreator,
eventId, eventId,
answer.text,
contract contract
) )
}) })

View File

@ -78,10 +78,10 @@ export const onCreateComment = functions
'created', 'created',
commentCreator, commentCreator,
eventId, eventId,
comment.text,
contract, contract,
relatedSourceType, relatedSourceType,
relatedUser, relatedUser
comment.text
) )
const recipientUserIds = uniq([ const recipientUserIds = uniq([

View File

@ -0,0 +1,24 @@
import * as functions from 'firebase-functions'
import { getUser } from './utils'
import { createNotification } from './create-notification'
import { Contract } from '../../common/contract'
export const onCreateContract = functions.firestore
.document('contracts/{contractId}')
.onCreate(async (snapshot, context) => {
const contract = snapshot.data() as Contract
const { eventId } = context
const contractCreator = await getUser(contract.creatorId)
if (!contractCreator) throw new Error('Could not find contract creator')
await createNotification(
contract.id,
'contract',
'created',
contractCreator,
eventId,
contract.question,
contract
)
})

View File

@ -21,6 +21,7 @@ export const onFollowUser = functions.firestore
'created', 'created',
followingUser, followingUser,
eventId, eventId,
'',
undefined, undefined,
undefined, undefined,
follow.userId follow.userId

View File

@ -20,6 +20,7 @@ export const onUpdateContract = functions.firestore
'resolved', 'resolved',
contractUpdater, contractUpdater,
eventId, eventId,
contract.question,
contract contract
) )
} else if ( } else if (
@ -32,6 +33,7 @@ export const onUpdateContract = functions.firestore
'updated', 'updated',
contractUpdater, contractUpdater,
eventId, eventId,
contract.question,
contract contract
) )
} }

View File

@ -1,7 +1,5 @@
import { Contract } from 'common/contract'
import React, { useState } from 'react' import React, { useState } from 'react'
import { ENV_CONFIG } from 'common/envs/constants' import { ENV_CONFIG } from 'common/envs/constants'
import { contractPath } from 'web/lib/firebase/contracts'
import { copyToClipboard } from 'web/lib/util/copy' import { copyToClipboard } from 'web/lib/util/copy'
import { DateTimeTooltip } from 'web/components/datetime-tooltip' import { DateTimeTooltip } from 'web/components/datetime-tooltip'
import Link from 'next/link' import Link from 'next/link'
@ -11,20 +9,26 @@ import { LinkIcon } from '@heroicons/react/outline'
import clsx from 'clsx' import clsx from 'clsx'
export function CopyLinkDateTimeComponent(props: { export function CopyLinkDateTimeComponent(props: {
contract: Contract contractCreatorUsername: string
contractSlug: string
createdTime: number createdTime: number
elementId: string elementId: string
className?: string className?: string
}) { }) {
const { contract, elementId, createdTime, className } = props const {
contractCreatorUsername,
contractSlug,
elementId,
createdTime,
className,
} = props
const [showToast, setShowToast] = useState(false) const [showToast, setShowToast] = useState(false)
function copyLinkToComment( function copyLinkToComment(
event: React.MouseEvent<HTMLAnchorElement, MouseEvent> event: React.MouseEvent<HTMLAnchorElement, MouseEvent>
) { ) {
event.preventDefault() event.preventDefault()
const elementLocation = `https://${ENV_CONFIG.domain}${contractPath( const elementLocation = `https://${ENV_CONFIG.domain}/${contractCreatorUsername}/${contractSlug}
contract
)}#${elementId}` )}#${elementId}`
copyToClipboard(elementLocation) copyToClipboard(elementLocation)
@ -35,7 +39,7 @@ export function CopyLinkDateTimeComponent(props: {
<div className={clsx('inline', className)}> <div className={clsx('inline', className)}>
<DateTimeTooltip time={createdTime}> <DateTimeTooltip time={createdTime}>
<Link <Link
href={`/${contract.creatorUsername}/${contract.slug}#${elementId}`} href={`/${contractCreatorUsername}/${contractCreatorUsername}#${elementId}`}
passHref={true} passHref={true}
> >
<a <a

View File

@ -127,7 +127,8 @@ export function FeedAnswerCommentGroup(props: {
<div className="text-sm text-gray-500"> <div className="text-sm text-gray-500">
<UserLink username={username} name={name} /> answered <UserLink username={username} name={name} /> answered
<CopyLinkDateTimeComponent <CopyLinkDateTimeComponent
contract={contract} contractCreatorUsername={contract.creatorUsername}
contractSlug={contract.slug}
createdTime={answer.createdTime} createdTime={answer.createdTime}
elementId={answerElementId} elementId={answerElementId}
/> />

View File

@ -245,7 +245,8 @@ export function FeedComment(props: {
)} )}
</> </>
<CopyLinkDateTimeComponent <CopyLinkDateTimeComponent
contract={contract} contractCreatorUsername={contract.creatorUsername}
contractSlug={contract.slug}
createdTime={createdTime} createdTime={createdTime}
elementId={comment.id} elementId={comment.id}
/> />

View File

@ -17,8 +17,8 @@ export function usePreferredGroupedNotifications(
options: { unseenOnly: boolean } options: { unseenOnly: boolean }
) { ) {
const [notificationGroups, setNotificationGroups] = useState< const [notificationGroups, setNotificationGroups] = useState<
NotificationGroup[] NotificationGroup[] | undefined
>([]) >(undefined)
const notifications = usePreferredNotifications(userId, options) const notifications = usePreferredNotifications(userId, options)
useEffect(() => { useEffect(() => {

View File

@ -45,16 +45,17 @@ import toast from 'react-hot-toast'
export default function Notifications() { export default function Notifications() {
const user = useUser() const user = useUser()
const [unseenNotificationGroups, setUnseenNotificationGroups] = useState< const [unseenNotificationGroups, setUnseenNotificationGroups] = useState<
NotificationGroup[] NotificationGroup[] | undefined
>([]) >(undefined)
const allNotificationGroups = usePreferredGroupedNotifications(user?.id, { const allNotificationGroups = usePreferredGroupedNotifications(user?.id, {
unseenOnly: false, unseenOnly: false,
}) })
useEffect(() => { useEffect(() => {
if (!allNotificationGroups) return
// Don't re-add notifications that are visible right now or have been seen already. // Don't re-add notifications that are visible right now or have been seen already.
const currentlyVisibleUnseenNotificationIds = Object.values( const currentlyVisibleUnseenNotificationIds = Object.values(
unseenNotificationGroups unseenNotificationGroups ?? []
) )
.map((n) => n.notifications.map((n) => n.id)) .map((n) => n.notifications.map((n) => n.id))
.flat() .flat()
@ -92,7 +93,7 @@ export default function Notifications() {
tabs={[ tabs={[
{ {
title: 'New Notifications', title: 'New Notifications',
content: ( content: unseenNotificationGroups ? (
<div className={''}> <div className={''}>
{unseenNotificationGroups.length === 0 && {unseenNotificationGroups.length === 0 &&
"You don't have any new notifications."} "You don't have any new notifications."}
@ -113,11 +114,13 @@ export default function Notifications() {
) )
)} )}
</div> </div>
) : (
<LoadingIndicator />
), ),
}, },
{ {
title: 'All Notifications', title: 'All Notifications',
content: ( content: allNotificationGroups ? (
<div className={''}> <div className={''}>
{allNotificationGroups.length === 0 && {allNotificationGroups.length === 0 &&
"You don't have any notifications. Try changing your settings to see more."} "You don't have any notifications. Try changing your settings to see more."}
@ -138,6 +141,8 @@ export default function Notifications() {
) )
)} )}
</div> </div>
) : (
<LoadingIndicator />
), ),
}, },
{ {
@ -469,10 +474,8 @@ function isNotificationAboutContractResolution(
contract: Contract | null | undefined contract: Contract | null | undefined
) { ) {
return ( return (
(sourceType === 'contract' && !sourceUpdateType && contract?.resolution) || (sourceType === 'contract' && sourceUpdateType === 'resolved') ||
(sourceType === 'contract' && (sourceType === 'contract' && !sourceUpdateType && contract?.resolution)
sourceUpdateType === 'resolved' &&
contract?.resolution)
) )
} }
@ -492,32 +495,58 @@ function NotificationItem(props: {
reason, reason,
sourceUserUsername, sourceUserUsername,
createdTime, createdTime,
sourceText,
sourceContractTitle,
sourceContractCreatorUsername,
sourceContractSlug,
} = notification } = notification
const [notificationText, setNotificationText] = useState<string>('') const [notificationText, setNotificationText] = useState<string>('')
const [contract, setContract] = useState<Contract | null>(null) const [contract, setContract] = useState<Contract | null>(null)
useEffect(() => { useEffect(() => {
if (!sourceContractId) return if (
getContractFromId(sourceContractId).then((contract) => { !sourceContractId ||
if (contract) setContract(contract) (sourceContractSlug && sourceContractCreatorUsername)
}) )
}, [sourceContractId]) return
getContractFromId(sourceContractId)
.then((contract) => {
if (contract) setContract(contract)
})
.catch((e) => console.log(e))
}, [
sourceContractCreatorUsername,
sourceContractId,
sourceContractSlug,
sourceContractTitle,
])
useEffect(() => { useEffect(() => {
if (!contract || !sourceContractId || !sourceId) return
if ( if (
sourceText &&
(sourceType === 'comment' ||
sourceType === 'answer' ||
(sourceType === 'contract' && sourceUpdateType === 'updated'))
) {
setNotificationText(sourceText)
} else if (!contract || !sourceContractId || !sourceId) return
else if (
sourceType === 'answer' || sourceType === 'answer' ||
sourceType === 'comment' || sourceType === 'comment' ||
sourceType === 'contract' sourceType === 'contract'
) { ) {
getNotificationText( try {
sourceId, getNotificationText(
sourceContractId, sourceId,
sourceType, sourceContractId,
sourceUpdateType, sourceType,
setNotificationText, sourceUpdateType,
contract setNotificationText,
) contract
)
} catch (err) {
console.error(err)
}
} else if (reasonText) { } else if (reasonText) {
// Handle arbitrary notifications with reason text here. // Handle arbitrary notifications with reason text here.
setNotificationText(reasonText) setNotificationText(reasonText)
@ -527,6 +556,7 @@ function NotificationItem(props: {
reasonText, reasonText,
sourceContractId, sourceContractId,
sourceId, sourceId,
sourceText,
sourceType, sourceType,
sourceUpdateType, sourceUpdateType,
]) ])
@ -537,6 +567,10 @@ function NotificationItem(props: {
function getSourceUrl() { function getSourceUrl() {
if (sourceType === 'follow') return `/${sourceUserUsername}` if (sourceType === 'follow') return `/${sourceUserUsername}`
if (sourceContractCreatorUsername && sourceContractSlug)
return `/${sourceContractCreatorUsername}/${sourceContractSlug}#${getSourceIdForLinkComponent(
sourceId ?? ''
)}`
if (!contract) return '' if (!contract) return ''
return `/${contract.creatorUsername}/${ return `/${contract.creatorUsername}/${
contract.slug contract.slug
@ -562,19 +596,16 @@ function NotificationItem(props: {
sourceType: 'answer' | 'comment' | 'contract', sourceType: 'answer' | 'comment' | 'contract',
sourceUpdateType: notification_source_update_types | undefined, sourceUpdateType: notification_source_update_types | undefined,
setText: (text: string) => void, setText: (text: string) => void,
contract?: Contract contract: Contract
) { ) {
if (sourceType === 'contract' && !contract) if (sourceType === 'contract') {
contract = await getContractFromId(sourceContractId)
if (sourceType === 'contract' && contract) {
if ( if (
isNotificationAboutContractResolution( isNotificationAboutContractResolution(
sourceType, sourceType,
sourceUpdateType, sourceUpdateType,
contract contract
) && ) &&
contract?.resolution contract.resolution
) )
setText(contract.resolution) setText(contract.resolution)
else setText(contract.question) else setText(contract.question)
@ -602,31 +633,24 @@ function NotificationItem(props: {
className={'mr-0 flex-shrink-0'} className={'mr-0 flex-shrink-0'}
/> />
<div className={'inline-flex overflow-hidden text-ellipsis pl-1'}> <div className={'inline-flex overflow-hidden text-ellipsis pl-1'}>
{sourceType && <span className={'flex-shrink-0'}>
reason && {sourceType &&
getReasonForShowingNotification( reason &&
sourceType, getReasonForShowingNotification(
reason, sourceType,
sourceUpdateType, reason,
contract, sourceUpdateType,
true contract,
).replace(' on', '')} true
).replace(' on', '')}
</span>
<div className={'ml-1 text-black'}> <div className={'ml-1 text-black'}>
{contract ? ( <NotificationTextLabel
<NotificationTextLabel contract={contract}
contract={contract} defaultText={notificationText}
defaultText={notificationText} className={'line-clamp-1'}
className={'line-clamp-1'} notification={notification}
sourceUpdateType={sourceUpdateType} />
sourceType={sourceType}
/>
) : sourceType != 'follow' ? (
<LoadingIndicator
spinnerClassName={'border-gray-500 h-4 w-4'}
/>
) : (
<div />
)}
</div> </div>
</div> </div>
</div> </div>
@ -666,35 +690,40 @@ function NotificationItem(props: {
contract contract
)} )}
<span className={'mx-1 font-bold'}> <span className={'mx-1 font-bold'}>
{contract?.question} {contract?.question || sourceContractTitle}
</span> </span>
</div> </div>
)} )}
</div> </div>
</div> </div>
{contract && sourceId && ( {sourceId && contract && (
<CopyLinkDateTimeComponent <CopyLinkDateTimeComponent
contract={contract} contractCreatorUsername={contract.creatorUsername}
contractSlug={contract.slug}
createdTime={createdTime} createdTime={createdTime}
elementId={getSourceIdForLinkComponent(sourceId)} elementId={getSourceIdForLinkComponent(sourceId)}
className={'-mx-1 inline-flex sm:inline-block'} className={'-mx-1 inline-flex sm:inline-block'}
/> />
)} )}
{sourceId &&
sourceContractSlug &&
sourceContractCreatorUsername && (
<CopyLinkDateTimeComponent
contractCreatorUsername={sourceContractCreatorUsername}
contractSlug={sourceContractSlug}
createdTime={createdTime}
elementId={getSourceIdForLinkComponent(sourceId)}
className={'-mx-1 inline-flex sm:inline-block'}
/>
)}
</div> </div>
</Row> </Row>
<div className={'mt-1 md:text-base'}> <div className={'mt-1 md:text-base'}>
{contract ? ( <NotificationTextLabel
<NotificationTextLabel contract={contract}
contract={contract} defaultText={notificationText}
defaultText={notificationText} notification={notification}
sourceType={sourceType} />
sourceUpdateType={sourceUpdateType}
/>
) : sourceType != 'follow' ? (
<LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
) : (
<div />
)}
</div> </div>
<div className={'mt-6 border-b border-gray-300'} /> <div className={'mt-6 border-b border-gray-300'} />
@ -704,14 +733,21 @@ function NotificationItem(props: {
} }
function NotificationTextLabel(props: { function NotificationTextLabel(props: {
contract: Contract
defaultText: string defaultText: string
sourceType?: notification_source_types contract?: Contract | null
sourceUpdateType?: notification_source_update_types notification: Notification
className?: string className?: string
}) { }) {
const { contract, className, sourceUpdateType, sourceType, defaultText } = const { contract, className, defaultText, notification } = props
props const { sourceUpdateType, sourceType, sourceText, sourceContractTitle } =
notification
if (
!contract &&
!sourceContractTitle &&
!sourceText &&
sourceType !== 'follow'
)
return <LoadingIndicator spinnerClassName={'border-gray-500 h-4 w-4'} />
if ( if (
isNotificationAboutContractResolution( isNotificationAboutContractResolution(
@ -719,7 +755,7 @@ function NotificationTextLabel(props: {
sourceUpdateType, sourceUpdateType,
contract contract
) && ) &&
contract.resolution contract?.resolution
) { ) {
if (contract.outcomeType === 'FREE_RESPONSE') { if (contract.outcomeType === 'FREE_RESPONSE') {
return ( return (
@ -782,7 +818,8 @@ function getReasonForShowingNotification(
else reasonText = `commented on` else reasonText = `commented on`
break break
case 'contract': case 'contract':
if ( if (reason === 'you_follow_user') reasonText = 'created a new question'
else if (
isNotificationAboutContractResolution( isNotificationAboutContractResolution(
source, source,
sourceUpdateType, sourceUpdateType,
@ -794,7 +831,8 @@ function getReasonForShowingNotification(
break break
case 'answer': case 'answer':
if (reason === 'on_users_contract') reasonText = `answered your question ` if (reason === 'on_users_contract') reasonText = `answered your question `
if (reason === 'on_contract_with_users_comment') reasonText = `answered` else if (reason === 'on_contract_with_users_comment')
reasonText = `answered`
else if (reason === 'on_contract_with_users_answer') else if (reason === 'on_contract_with_users_answer')
reasonText = `answered` reasonText = `answered`
else if (reason === 'on_contract_with_users_shares_in') else if (reason === 'on_contract_with_users_shares_in')