From 9c49f2e2d729f4526630b2e4b0a7c6a3538df54a Mon Sep 17 00:00:00 2001 From: Ian Philips Date: Fri, 15 Jul 2022 06:52:08 -0600 Subject: [PATCH] Revert "Revert "Order groups by most recent chat activity (#650)"" This reverts commit 17c9beca2846b358505b3048a1c7b731eb931dfc. --- common/group.ts | 3 ++ functions/src/create-notification.ts | 49 ++++++++++++++------- functions/src/on-create-comment-on-group.ts | 18 +++----- functions/src/on-update-group.ts | 10 ++++- web/components/groups/groups-button.tsx | 4 +- web/components/nav/sidebar.tsx | 20 +++++++-- web/components/user-page.tsx | 5 ++- web/hooks/use-group.ts | 27 +++++++----- web/lib/firebase/groups.ts | 19 +++++--- web/pages/group/[...slugs]/index.tsx | 23 ++++++---- web/pages/groups.tsx | 6 ++- 11 files changed, 125 insertions(+), 59 deletions(-) diff --git a/common/group.ts b/common/group.ts index 15348d5a..e367ded7 100644 --- a/common/group.ts +++ b/common/group.ts @@ -11,8 +11,11 @@ export type Group = { contractIds: string[] chatDisabled?: boolean + mostRecentChatActivityTime?: number + mostRecentContractAddedTime?: number } export const MAX_GROUP_NAME_LENGTH = 75 export const MAX_ABOUT_LENGTH = 140 export const MAX_ID_LENGTH = 60 export const NEW_USER_GROUP_SLUGS = ['updates', 'bugs', 'welcome'] +export const GROUP_CHAT_SLUG = 'chat' diff --git a/functions/src/create-notification.ts b/functions/src/create-notification.ts index 1fb6c3af..4c42b00e 100644 --- a/functions/src/create-notification.ts +++ b/functions/src/create-notification.ts @@ -15,11 +15,11 @@ import { Answer } from '../../common/answer' import { getContractBetMetrics } from '../../common/calculate' import { removeUndefinedProps } from '../../common/util/object' import { TipTxn } from '../../common/txn' -import { Group } from '../../common/group' +import { Group, GROUP_CHAT_SLUG } from '../../common/group' const firestore = admin.firestore() type user_to_reason_texts = { - [userId: string]: { reason: notification_reason_types; isSeeOnHref?: string } + [userId: string]: { reason: notification_reason_types } } export const createNotification = async ( @@ -72,7 +72,6 @@ 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)) }) @@ -277,17 +276,6 @@ export const createNotification = async ( } } - const notifyOtherGroupMembersOfComment = async ( - userToReasons: user_to_reason_texts, - userId: string - ) => { - if (shouldGetNotification(userId, userToReasons)) - userToReasons[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. @@ -298,8 +286,6 @@ 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. @@ -417,3 +403,34 @@ export const createBetFillNotification = async ( } return await notificationRef.set(removeUndefinedProps(notification)) } + +export const createGroupCommentNotification = async ( + fromUser: User, + toUserId: string, + comment: Comment, + group: Group, + idempotencyKey: string +) => { + const notificationRef = firestore + .collection(`/users/${toUserId}/notifications`) + .doc(idempotencyKey) + const sourceSlug = `/group/${group.slug}/${GROUP_CHAT_SLUG}` + const notification: Notification = { + id: idempotencyKey, + userId: toUserId, + reason: 'on_group_you_are_member_of', + createdTime: Date.now(), + isSeen: false, + sourceId: comment.id, + sourceType: 'comment', + sourceUpdateType: 'created', + sourceUserName: fromUser.name, + sourceUserUsername: fromUser.username, + sourceUserAvatarUrl: fromUser.avatarUrl, + sourceText: comment.text, + sourceSlug, + sourceTitle: `${group.name}`, + isSeenOnHref: sourceSlug, + } + await notificationRef.set(removeUndefinedProps(notification)) +} diff --git a/functions/src/on-create-comment-on-group.ts b/functions/src/on-create-comment-on-group.ts index 7217e602..0064480f 100644 --- a/functions/src/on-create-comment-on-group.ts +++ b/functions/src/on-create-comment-on-group.ts @@ -3,7 +3,7 @@ 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' +import { createGroupCommentNotification } from './create-notification' const firestore = admin.firestore() export const onCreateCommentOnGroup = functions.firestore @@ -29,23 +29,17 @@ export const onCreateCommentOnGroup = functions.firestore const group = groupSnapshot.data() as Group await firestore.collection('groups').doc(groupId).update({ - mostRecentActivityTime: comment.createdTime, + mostRecentChatActivityTime: comment.createdTime, }) await Promise.all( group.memberIds.map(async (memberId) => { - return await createNotification( - comment.id, - 'comment', - 'created', + return await createGroupCommentNotification( creatorSnapshot.data() as User, - eventId, - comment.text, - undefined, - undefined, memberId, - `/group/${group.slug}`, - `${group.name}` + comment, + group, + eventId ) }) ) diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index feaa6443..3ab2a249 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -12,7 +12,15 @@ 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 + + if (prevGroup.contractIds.length < group.contractIds.length) { + await firestore + .collection('groups') + .doc(group.id) + .update({ mostRecentContractAddedTime: Date.now() }) + //TODO: create notification with isSeeOnHref set to the group's /group/slug/questions url + // but first, let the new /group/slug/chat notification permeate so that we can differentiate between the two + } await firestore .collection('groups') diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index f3ae77a2..b510f44d 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -17,7 +17,9 @@ import toast from 'react-hot-toast' export function GroupsButton(props: { user: User }) { const { user } = props const [isOpen, setIsOpen] = useState(false) - const groups = useMemberGroups(user.id) + const groups = useMemberGroups(user.id, undefined, { + by: 'mostRecentChatActivityTime', + }) return ( <> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 1ff59275..baa60719 100644 --- a/web/components/nav/sidebar.tsx +++ b/web/components/nav/sidebar.tsx @@ -24,7 +24,7 @@ import { CreateQuestionButton } from 'web/components/create-question-button' import { useMemberGroups } from 'web/hooks/use-group' import { groupPath } from 'web/lib/firebase/groups' import { trackCallback, withTracking } from 'web/lib/service/analytics' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -198,10 +198,14 @@ export default function Sidebar(props: { className?: string }) { ? signedOutMobileNavigation : signedInMobileNavigation const memberItems = ( - useMemberGroups(user?.id, { withChatEnabled: true }) ?? [] + useMemberGroups( + user?.id, + { withChatEnabled: true }, + { by: 'mostRecentChatActivityTime' } + ) ?? [] ).map((group: Group) => ({ name: group.name, - href: groupPath(group.slug), + href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, })) return ( @@ -282,8 +286,16 @@ function GroupsList(props: { // Set notification as seen if our current page is equal to the isSeenOnHref property useEffect(() => { + const currentPageGroupSlug = currentPage.split('/')[2] preferredNotifications.forEach((notification) => { - if (notification.isSeenOnHref === currentPage) { + if ( + notification.isSeenOnHref === currentPage || + // Old chat style group chat notif ended just with the group slug + notification.isSeenOnHref?.endsWith(currentPageGroupSlug) || + // They're on the home page, so if they've a chat notif, they're seeing the chat + (notification.isSeenOnHref?.endsWith(GROUP_CHAT_SLUG) && + currentPage.endsWith(currentPageGroupSlug)) + ) { setNotificationsAsSeen([notification]) } }) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index be3f3ac4..85d70e86 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -38,6 +38,7 @@ import { GroupsButton } from 'web/components/groups/groups-button' import { PortfolioValueSection } from './portfolio/portfolio-value-section' import { filterDefined } from 'common/util/array' import { useUserBets } from 'web/hooks/use-user-bets' +import { ReferralsButton } from 'web/components/referrals-button' export function UserLink(props: { name: string @@ -202,7 +203,9 @@ export function UserPage(props: { - {/* */} + {currentUser?.username === 'Ian' && ( + + )} diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index c3098ba4..4f968005 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -32,19 +32,26 @@ export const useGroups = () => { export const useMemberGroups = ( userId: string | null | undefined, - options?: { withChatEnabled: boolean } + options?: { withChatEnabled: boolean }, + sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } ) => { const [memberGroups, setMemberGroups] = useState() useEffect(() => { if (userId) - return listenForMemberGroups(userId, (groups) => { - if (options?.withChatEnabled) - return setMemberGroups( - filterDefined(groups.filter((group) => group.chatDisabled !== true)) - ) - return setMemberGroups(groups) - }) - }, [options?.withChatEnabled, userId]) + return listenForMemberGroups( + userId, + (groups) => { + if (options?.withChatEnabled) + return setMemberGroups( + filterDefined( + groups.filter((group) => group.chatDisabled !== true) + ) + ) + return setMemberGroups(groups) + }, + sort + ) + }, [options?.withChatEnabled, sort, userId]) return memberGroups } @@ -88,7 +95,7 @@ export async function listMembers(group: Group, max?: number) { const { memberIds } = group const numToRetrieve = max ?? memberIds.length if (memberIds.length === 0) return [] - if (numToRetrieve) + if (numToRetrieve > 100) return (await getUsers()).filter((user) => group.memberIds.includes(user.id) ) diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 6d695b7f..e49b012a 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -7,7 +7,7 @@ import { where, } from 'firebase/firestore' import { sortBy, uniq } from 'lodash' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { updateContract } from './contracts' import { coll, @@ -22,7 +22,7 @@ export const groups = coll('groups') export function groupPath( groupSlug: string, - subpath?: 'edit' | 'questions' | 'about' | 'chat' | 'rankings' + subpath?: 'edit' | 'questions' | 'about' | typeof GROUP_CHAT_SLUG | 'rankings' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } @@ -62,12 +62,21 @@ export function listenForGroup( export function listenForMemberGroups( userId: string, - setGroups: (groups: Group[]) => void + setGroups: (groups: Group[]) => void, + sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } ) { const q = query(groups, where('memberIds', 'array-contains', userId)) - + const sorter = (group: Group) => { + if (sort?.by === 'mostRecentChatActivityTime') { + return group.mostRecentChatActivityTime ?? group.mostRecentActivityTime + } + if (sort?.by === 'mostRecentContractAddedTime') { + return group.mostRecentContractAddedTime ?? group.mostRecentActivityTime + } + return group.mostRecentActivityTime + } return listenForValues(q, (groups) => { - const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime]) + const sorted = sortBy(groups, [(group) => -sorter(group)]) setGroups(sorted) }) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index a364de43..3fa64964 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -1,6 +1,6 @@ import { take, sortBy, debounce } from 'lodash' -import { Group } from 'common/group' +import { Group, GROUP_CHAT_SLUG } from 'common/group' import { Page } from 'web/components/page' import { listAllBets } from 'web/lib/firebase/bets' import { Contract, listContractsByGroupSlug } from 'web/lib/firebase/contracts' @@ -21,7 +21,7 @@ import { } from 'web/lib/firebase/users' import { Col } from 'web/components/layout/col' import { useUser } from 'web/hooks/use-user' -import { listMembers, useGroup } from 'web/hooks/use-group' +import { listMembers, useGroup, useMembers } from 'web/hooks/use-group' import { useRouter } from 'next/router' import { scoreCreators, scoreTraders } from 'common/scoring' import { Leaderboard } from 'web/components/leaderboard' @@ -114,7 +114,7 @@ export async function getStaticPaths() { } const groupSubpages = [ undefined, - 'chat', + GROUP_CHAT_SLUG, 'questions', 'rankings', 'about', @@ -218,7 +218,7 @@ export default function GroupPage(props: { ) : ( ), - href: groupPath(group.slug, 'chat'), + href: groupPath(group.slug, GROUP_CHAT_SLUG), }, ]), { @@ -246,7 +246,7 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'about'), }, ] - const tabIndex = tabs.map((t) => t.title).indexOf(page ?? 'chat') + const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) return ( )} - + @@ -426,9 +426,16 @@ function SearchBar(props: { setQuery: (query: string) => void }) { ) } -function GroupMemberSearch(props: { members: User[] }) { +function GroupMemberSearch(props: { members: User[]; group: Group }) { const [query, setQuery] = useState('') - const { members } = props + const { group } = props + let { members } = props + + // Use static members on load, but also listen to member changes: + const listenToMembers = useMembers(group) + if (listenToMembers) { + members = listenToMembers + } // TODO use find-active-contracts to sort by? const matches = sortBy(members, [(member) => member.name]).filter( diff --git a/web/pages/groups.tsx b/web/pages/groups.tsx index 2523b789..87ac1501 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -79,7 +79,11 @@ export default function Groups(props: { ) const matchesOrderedByRecentActivity = sortBy(groups, [ - (group) => -1 * group.mostRecentActivityTime, + (group) => + -1 * + (group.mostRecentChatActivityTime ?? + group.mostRecentContractAddedTime ?? + group.mostRecentActivityTime), ]).filter( (g) => checkAgainstQuery(query, g.name) ||