Bold groups with recent chat activity (#621)

* Bold groups with recent chat activity

* Cleanup

* Cleanup
This commit is contained in:
Ian Philips 2022-07-05 17:18:37 -06:00 committed by GitHub
parent 270a5fc139
commit 3a6d28e2c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 16 deletions

View File

@ -22,6 +22,8 @@ export type Notification = {
sourceSlug?: string sourceSlug?: string
sourceTitle?: string sourceTitle?: string
isSeenOnHref?: string
} }
export type notification_source_types = export type notification_source_types =
| 'contract' | 'contract'
@ -58,3 +60,4 @@ export type notification_reason_types =
| 'you_referred_user' | 'you_referred_user'
| 'user_joined_to_bet_on_your_market' | 'user_joined_to_bet_on_your_market'
| 'unique_bettors_on_your_contract' | 'unique_bettors_on_your_contract'
| 'on_group_you_are_member_of'

View File

@ -17,7 +17,7 @@ import { removeUndefinedProps } from '../../common/util/object'
const firestore = admin.firestore() const firestore = admin.firestore()
type user_to_reason_texts = { type user_to_reason_texts = {
[userId: string]: { reason: notification_reason_types } [userId: string]: { reason: notification_reason_types; isSeeOnHref?: string }
} }
export const createNotification = async ( export const createNotification = async (
@ -72,6 +72,7 @@ export const createNotification = async (
sourceContractSlug: sourceContract?.slug, sourceContractSlug: sourceContract?.slug,
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug, sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question, sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
isSeenOnHref: userToReasonTexts[userId].isSeeOnHref,
} }
await notificationRef.set(removeUndefinedProps(notification)) await notificationRef.set(removeUndefinedProps(notification))
}) })
@ -276,6 +277,17 @@ export const createNotification = async (
} }
} }
const notifyOtherGroupMembersOfComment = async (
userToReasonTexts: user_to_reason_texts,
userId: string
) => {
if (shouldGetNotification(userId, userToReasonTexts))
userToReasonTexts[userId] = {
reason: 'on_group_you_are_member_of',
isSeeOnHref: sourceSlug,
}
}
const getUsersToNotify = async () => { const getUsersToNotify = async () => {
const userToReasonTexts: user_to_reason_texts = {} const userToReasonTexts: user_to_reason_texts = {}
// The following functions modify the userToReasonTexts object in place. // The following functions modify the userToReasonTexts object in place.
@ -286,6 +298,8 @@ export const createNotification = async (
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId) await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
} else if (sourceType === 'user' && relatedUserId) { } else if (sourceType === 'user' && relatedUserId) {
await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId) await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId)
} else if (sourceType === 'comment' && !sourceContract && relatedUserId) {
await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId)
} }
// The following functions need sourceContract to be defined. // The following functions need sourceContract to be defined.

View File

@ -10,7 +10,7 @@ export * from './stripe'
export * from './create-user' export * from './create-user'
export * from './create-answer' export * from './create-answer'
export * from './on-create-bet' export * from './on-create-bet'
export * from './on-create-comment' export * from './on-create-comment-on-contract'
export * from './on-view' export * from './on-view'
export * from './unsubscribe' export * from './unsubscribe'
export * from './update-metrics' export * from './update-metrics'
@ -28,6 +28,7 @@ export * from './on-create-liquidity-provision'
export * from './on-update-group' export * from './on-update-group'
export * from './on-create-group' export * from './on-create-group'
export * from './on-update-user' export * from './on-update-user'
export * from './on-create-comment-on-group'
// v2 // v2
export * from './health' export * from './health'

View File

@ -11,7 +11,7 @@ import { createNotification } from './create-notification'
const firestore = admin.firestore() const firestore = admin.firestore()
export const onCreateComment = functions export const onCreateCommentOnContract = functions
.runWith({ secrets: ['MAILGUN_KEY'] }) .runWith({ secrets: ['MAILGUN_KEY'] })
.firestore.document('contracts/{contractId}/comments/{commentId}') .firestore.document('contracts/{contractId}/comments/{commentId}')
.onCreate(async (change, context) => { .onCreate(async (change, context) => {

View File

@ -0,0 +1,52 @@
import * as functions from 'firebase-functions'
import { Comment } from '../../common/comment'
import * as admin from 'firebase-admin'
import { Group } from '../../common/group'
import { User } from '../../common/user'
import { createNotification } from './create-notification'
const firestore = admin.firestore()
export const onCreateCommentOnGroup = functions.firestore
.document('groups/{groupId}/comments/{commentId}')
.onCreate(async (change, context) => {
const { eventId } = context
const { groupId } = context.params as {
groupId: string
}
const comment = change.data() as Comment
const creatorSnapshot = await firestore
.collection('users')
.doc(comment.userId)
.get()
if (!creatorSnapshot.exists) throw new Error('Could not find user')
const groupSnapshot = await firestore
.collection('groups')
.doc(groupId)
.get()
if (!groupSnapshot.exists) throw new Error('Could not find group')
const group = groupSnapshot.data() as Group
await firestore.collection('groups').doc(groupId).update({
mostRecentActivityTime: comment.createdTime,
})
await Promise.all(
group.memberIds.map(async (memberId) => {
return await createNotification(
comment.id,
'comment',
'created',
creatorSnapshot.data() as User,
eventId,
comment.text,
undefined,
undefined,
memberId,
`/group/${group.slug}`,
`${group.name}`
)
})
)
})

View File

@ -12,6 +12,7 @@ export const onUpdateGroup = functions.firestore
// ignore the update we just made // ignore the update we just made
if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime)
return return
// TODO: create notification with isSeeOnHref set to the group's /group/questions url
await firestore await firestore
.collection('groups') .collection('groups')

View File

@ -18,7 +18,7 @@ import { ManifoldLogo } from './manifold-logo'
import { MenuButton } from './menu' import { MenuButton } from './menu'
import { ProfileSummary } from './profile-menu' import { ProfileSummary } from './profile-menu'
import NotificationsIcon from 'web/components/notifications-icon' import NotificationsIcon from 'web/components/notifications-icon'
import React from 'react' import React, { useEffect } from 'react'
import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants' import { IS_PRIVATE_MANIFOLD } from 'common/envs/constants'
import { CreateQuestionButton } from 'web/components/create-question-button' import { CreateQuestionButton } from 'web/components/create-question-button'
import { useMemberGroups } from 'web/hooks/use-group' import { useMemberGroups } from 'web/hooks/use-group'
@ -26,6 +26,8 @@ import { groupPath } from 'web/lib/firebase/groups'
import { trackCallback, withTracking } from 'web/lib/service/analytics' import { trackCallback, withTracking } from 'web/lib/service/analytics'
import { Group } from 'common/group' import { Group } from 'common/group'
import { Spacer } from '../layout/spacer' import { Spacer } from '../layout/spacer'
import { usePreferredNotifications } from 'web/hooks/use-notifications'
import { setNotificationsAsSeen } from 'web/pages/notifications'
function getNavigation() { function getNavigation() {
return [ return [
@ -182,6 +184,7 @@ export default function Sidebar(props: { className?: string }) {
const { className } = props const { className } = props
const router = useRouter() const router = useRouter()
const currentPage = router.pathname const currentPage = router.pathname
const user = useUser() const user = useUser()
const navigationOptions = !user ? signedOutNavigation : getNavigation() const navigationOptions = !user ? signedOutNavigation : getNavigation()
const mobileNavigationOptions = !user const mobileNavigationOptions = !user
@ -217,7 +220,11 @@ export default function Sidebar(props: { className?: string }) {
/> />
)} )}
<GroupsList currentPage={currentPage} memberItems={memberItems} /> <GroupsList
currentPage={router.asPath}
memberItems={memberItems}
user={user}
/>
</div> </div>
{/* Desktop navigation */} {/* Desktop navigation */}
@ -236,14 +243,36 @@ export default function Sidebar(props: { className?: string }) {
<div className="h-[1px] bg-gray-300" /> <div className="h-[1px] bg-gray-300" />
</div> </div>
)} )}
<GroupsList currentPage={currentPage} memberItems={memberItems} /> <GroupsList
currentPage={router.asPath}
memberItems={memberItems}
user={user}
/>
</div> </div>
</nav> </nav>
) )
} }
function GroupsList(props: { currentPage: string; memberItems: Item[] }) { function GroupsList(props: {
const { currentPage, memberItems } = props currentPage: string
memberItems: Item[]
user: User | null | undefined
}) {
const { currentPage, memberItems, user } = props
const preferredNotifications = usePreferredNotifications(user?.id, {
unseenOnly: true,
customHref: '/group/',
})
// Set notification as seen if our current page is equal to the isSeenOnHref property
useEffect(() => {
preferredNotifications.forEach((notification) => {
if (notification.isSeenOnHref === currentPage) {
setNotificationsAsSeen([notification])
}
})
}, [currentPage, preferredNotifications])
return ( return (
<> <>
<SidebarItem <SidebarItem
@ -256,9 +285,14 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
<a <a
key={item.href} key={item.href}
href={item.href} href={item.href}
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900" className={clsx(
'group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900',
preferredNotifications.some(
(n) => !n.isSeen && n.isSeenOnHref === item.href
) && 'font-bold'
)}
> >
<span className="truncate">&nbsp; {item.name}</span> <span className="truncate">{item.name}</span>
</a> </a>
))} ))}
</div> </div>

View File

@ -83,11 +83,11 @@ export function groupNotifications(notifications: Notification[]) {
return notificationGroups return notificationGroups
} }
function usePreferredNotifications( export function usePreferredNotifications(
userId: string | undefined, userId: string | undefined,
options: { unseenOnly: boolean } options: { unseenOnly: boolean; customHref?: string }
) { ) {
const { unseenOnly } = options const { unseenOnly, customHref } = options
const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null) const [privateUser, setPrivateUser] = useState<PrivateUser | null>(null)
const [notifications, setNotifications] = useState<Notification[]>([]) const [notifications, setNotifications] = useState<Notification[]>([])
const [userAppropriateNotifications, setUserAppropriateNotifications] = const [userAppropriateNotifications, setUserAppropriateNotifications] =
@ -112,9 +112,11 @@ function usePreferredNotifications(
const notificationsToShow = getAppropriateNotifications( const notificationsToShow = getAppropriateNotifications(
notifications, notifications,
privateUser.notificationPreferences privateUser.notificationPreferences
).filter((n) =>
customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref
) )
setUserAppropriateNotifications(notificationsToShow) setUserAppropriateNotifications(notificationsToShow)
}, [privateUser, notifications]) }, [privateUser, notifications, customHref])
return userAppropriateNotifications return userAppropriateNotifications
} }

View File

@ -166,7 +166,7 @@ export default function Notifications() {
) )
} }
const setNotificationsAsSeen = (notifications: Notification[]) => { export const setNotificationsAsSeen = (notifications: Notification[]) => {
notifications.forEach((notification) => { notifications.forEach((notification) => {
if (!notification.isSeen) if (!notification.isSeen)
updateDoc( updateDoc(
@ -758,7 +758,7 @@ function NotificationItem(props: {
<div <div
className={clsx( className={clsx(
'bg-white px-2 pt-6 text-sm sm:px-4', 'bg-white px-2 pt-6 text-sm sm:px-4',
highlighted && 'bg-indigo-200' highlighted && 'bg-indigo-200 hover:bg-indigo-100'
)} )}
> >
<a href={getSourceUrl()}> <a href={getSourceUrl()}>