Bold groups with recent chat activity (#621)
* Bold groups with recent chat activity * Cleanup * Cleanup
This commit is contained in:
parent
270a5fc139
commit
3a6d28e2c2
|
@ -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'
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) => {
|
52
functions/src/on-create-comment-on-group.ts
Normal file
52
functions/src/on-create-comment-on-group.ts
Normal 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}`
|
||||
)
|
||||
})
|
||||
)
|
||||
})
|
|
@ -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')
|
||||
|
|
|
@ -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"> {item.name}</span>
|
||||
<span className="truncate">{item.name}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()}>
|
||||
|
|
Loading…
Reference in New Issue
Block a user