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
sourceTitle?: string
isSeenOnHref?: string
}
export type notification_source_types =
| 'contract'
@ -58,3 +60,4 @@ export type notification_reason_types =
| 'you_referred_user'
| 'user_joined_to_bet_on_your_market'
| '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()
type user_to_reason_texts = {
[userId: string]: { reason: notification_reason_types }
[userId: string]: { reason: notification_reason_types; isSeeOnHref?: string }
}
export const createNotification = async (
@ -72,6 +72,7 @@ export const createNotification = async (
sourceContractSlug: sourceContract?.slug,
sourceSlug: sourceSlug ? sourceSlug : sourceContract?.slug,
sourceTitle: sourceTitle ? sourceTitle : sourceContract?.question,
isSeenOnHref: userToReasonTexts[userId].isSeeOnHref,
}
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 userToReasonTexts: user_to_reason_texts = {}
// The following functions modify the userToReasonTexts object in place.
@ -286,6 +298,8 @@ export const createNotification = async (
await notifyUserAddedToGroup(userToReasonTexts, relatedUserId)
} else if (sourceType === 'user' && relatedUserId) {
await notifyUserReceivedReferralBonus(userToReasonTexts, relatedUserId)
} else if (sourceType === 'comment' && !sourceContract && relatedUserId) {
await notifyOtherGroupMembersOfComment(userToReasonTexts, relatedUserId)
}
// The following functions need sourceContract to be defined.

View File

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

View File

@ -11,7 +11,7 @@ import { createNotification } from './create-notification'
const firestore = admin.firestore()
export const onCreateComment = functions
export const onCreateCommentOnContract = functions
.runWith({ secrets: ['MAILGUN_KEY'] })
.firestore.document('contracts/{contractId}/comments/{commentId}')
.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
if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime)
return
// TODO: create notification with isSeeOnHref set to the group's /group/questions url
await firestore
.collection('groups')

View File

@ -18,7 +18,7 @@ import { ManifoldLogo } from './manifold-logo'
import { MenuButton } from './menu'
import { ProfileSummary } from './profile-menu'
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 { CreateQuestionButton } from 'web/components/create-question-button'
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 { Group } from 'common/group'
import { Spacer } from '../layout/spacer'
import { usePreferredNotifications } from 'web/hooks/use-notifications'
import { setNotificationsAsSeen } from 'web/pages/notifications'
function getNavigation() {
return [
@ -182,6 +184,7 @@ export default function Sidebar(props: { className?: string }) {
const { className } = props
const router = useRouter()
const currentPage = router.pathname
const user = useUser()
const navigationOptions = !user ? signedOutNavigation : getNavigation()
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>
{/* Desktop navigation */}
@ -236,14 +243,36 @@ export default function Sidebar(props: { className?: string }) {
<div className="h-[1px] bg-gray-300" />
</div>
)}
<GroupsList currentPage={currentPage} memberItems={memberItems} />
<GroupsList
currentPage={router.asPath}
memberItems={memberItems}
user={user}
/>
</div>
</nav>
)
}
function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
const { currentPage, memberItems } = props
function GroupsList(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 (
<>
<SidebarItem
@ -256,9 +285,14 @@ function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
<a
key={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>
))}
</div>

View File

@ -83,11 +83,11 @@ export function groupNotifications(notifications: Notification[]) {
return notificationGroups
}
function usePreferredNotifications(
export function usePreferredNotifications(
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 [notifications, setNotifications] = useState<Notification[]>([])
const [userAppropriateNotifications, setUserAppropriateNotifications] =
@ -112,9 +112,11 @@ function usePreferredNotifications(
const notificationsToShow = getAppropriateNotifications(
notifications,
privateUser.notificationPreferences
).filter((n) =>
customHref ? n.isSeenOnHref?.includes(customHref) : !n.isSeenOnHref
)
setUserAppropriateNotifications(notificationsToShow)
}, [privateUser, notifications])
}, [privateUser, notifications, customHref])
return userAppropriateNotifications
}

View File

@ -166,7 +166,7 @@ export default function Notifications() {
)
}
const setNotificationsAsSeen = (notifications: Notification[]) => {
export const setNotificationsAsSeen = (notifications: Notification[]) => {
notifications.forEach((notification) => {
if (!notification.isSeen)
updateDoc(
@ -758,7 +758,7 @@ function NotificationItem(props: {
<div
className={clsx(
'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()}>