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
|
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'
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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) => {
|
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
|
// 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')
|
||||||
|
|
|
@ -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"> {item.name}</span>
|
<span className="truncate">{item.name}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()}>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user