diff --git a/common/group.ts b/common/group.ts index e367ded7..15348d5a 100644 --- a/common/group.ts +++ b/common/group.ts @@ -11,11 +11,8 @@ 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 4c42b00e..1fb6c3af 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, GROUP_CHAT_SLUG } from '../../common/group' +import { Group } from '../../common/group' 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 ( + 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. @@ -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. @@ -403,34 +417,3 @@ 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 0064480f..7217e602 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 { createGroupCommentNotification } from './create-notification' +import { createNotification } from './create-notification' const firestore = admin.firestore() export const onCreateCommentOnGroup = functions.firestore @@ -29,17 +29,23 @@ export const onCreateCommentOnGroup = functions.firestore const group = groupSnapshot.data() as Group await firestore.collection('groups').doc(groupId).update({ - mostRecentChatActivityTime: comment.createdTime, + mostRecentActivityTime: comment.createdTime, }) await Promise.all( group.memberIds.map(async (memberId) => { - return await createGroupCommentNotification( + return await createNotification( + comment.id, + 'comment', + 'created', creatorSnapshot.data() as User, + eventId, + comment.text, + undefined, + undefined, memberId, - comment, - group, - eventId + `/group/${group.slug}`, + `${group.name}` ) }) ) diff --git a/functions/src/on-update-group.ts b/functions/src/on-update-group.ts index 3ab2a249..feaa6443 100644 --- a/functions/src/on-update-group.ts +++ b/functions/src/on-update-group.ts @@ -12,15 +12,7 @@ export const onUpdateGroup = functions.firestore // ignore the update we just made if (prevGroup.mostRecentActivityTime !== group.mostRecentActivityTime) return - - 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 - } + // TODO: create notification with isSeeOnHref set to the group's /group/questions url await firestore .collection('groups') diff --git a/web/components/groups/groups-button.tsx b/web/components/groups/groups-button.tsx index b510f44d..f3ae77a2 100644 --- a/web/components/groups/groups-button.tsx +++ b/web/components/groups/groups-button.tsx @@ -17,9 +17,7 @@ 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, undefined, { - by: 'mostRecentChatActivityTime', - }) + const groups = useMemberGroups(user.id) return ( <> diff --git a/web/components/nav/sidebar.tsx b/web/components/nav/sidebar.tsx index 8553b506..f4abc6c7 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, GROUP_CHAT_SLUG } from 'common/group' +import { Group } from 'common/group' import { Spacer } from '../layout/spacer' import { useUnseenPreferredNotifications } from 'web/hooks/use-notifications' import { setNotificationsAsSeen } from 'web/pages/notifications' @@ -194,14 +194,10 @@ export default function Sidebar(props: { className?: string }) { ? signedOutMobileNavigation : signedInMobileNavigation const memberItems = ( - useMemberGroups( - user?.id, - { withChatEnabled: true }, - { by: 'mostRecentChatActivityTime' } - ) ?? [] + useMemberGroups(user?.id, { withChatEnabled: true }) ?? [] ).map((group: Group) => ({ name: group.name, - href: `${groupPath(group.slug)}/${GROUP_CHAT_SLUG}`, + href: groupPath(group.slug), })) return ( @@ -282,16 +278,8 @@ 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 || - // 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)) - ) { + if (notification.isSeenOnHref === currentPage) { setNotificationsAsSeen([notification]) } }) diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 85d70e86..be3f3ac4 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -38,7 +38,6 @@ 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 @@ -203,9 +202,7 @@ export function UserPage(props: { - {currentUser?.username === 'Ian' && ( - - )} + {/* */} diff --git a/web/hooks/use-group.ts b/web/hooks/use-group.ts index 4f968005..c3098ba4 100644 --- a/web/hooks/use-group.ts +++ b/web/hooks/use-group.ts @@ -32,26 +32,19 @@ export const useGroups = () => { export const useMemberGroups = ( userId: string | null | undefined, - options?: { withChatEnabled: boolean }, - sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } + options?: { withChatEnabled: boolean } ) => { 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) - }, - sort - ) - }, [options?.withChatEnabled, sort, userId]) + return listenForMemberGroups(userId, (groups) => { + if (options?.withChatEnabled) + return setMemberGroups( + filterDefined(groups.filter((group) => group.chatDisabled !== true)) + ) + return setMemberGroups(groups) + }) + }, [options?.withChatEnabled, userId]) return memberGroups } @@ -95,7 +88,7 @@ export async function listMembers(group: Group, max?: number) { const { memberIds } = group const numToRetrieve = max ?? memberIds.length if (memberIds.length === 0) return [] - if (numToRetrieve > 100) + if (numToRetrieve) 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 e49b012a..6d695b7f 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, GROUP_CHAT_SLUG } from 'common/group' +import { Group } 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' | typeof GROUP_CHAT_SLUG | 'rankings' + subpath?: 'edit' | 'questions' | 'about' | 'chat' | 'rankings' ) { return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}` } @@ -62,21 +62,12 @@ export function listenForGroup( export function listenForMemberGroups( userId: string, - setGroups: (groups: Group[]) => void, - sort?: { by: 'mostRecentChatActivityTime' | 'mostRecentContractAddedTime' } + setGroups: (groups: Group[]) => void ) { 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) => -sorter(group)]) + const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime]) setGroups(sorted) }) } diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 3fa64964..a364de43 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, GROUP_CHAT_SLUG } from 'common/group' +import { Group } 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, useMembers } from 'web/hooks/use-group' +import { listMembers, useGroup } 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, - GROUP_CHAT_SLUG, + 'chat', 'questions', 'rankings', 'about', @@ -218,7 +218,7 @@ export default function GroupPage(props: { ) : ( ), - href: groupPath(group.slug, GROUP_CHAT_SLUG), + href: groupPath(group.slug, 'chat'), }, ]), { @@ -246,7 +246,7 @@ export default function GroupPage(props: { href: groupPath(group.slug, 'about'), }, ] - const tabIndex = tabs.map((t) => t.title).indexOf(page ?? GROUP_CHAT_SLUG) + const tabIndex = tabs.map((t) => t.title).indexOf(page ?? 'chat') return ( )} - + @@ -426,16 +426,9 @@ function SearchBar(props: { setQuery: (query: string) => void }) { ) } -function GroupMemberSearch(props: { members: User[]; group: Group }) { +function GroupMemberSearch(props: { members: User[] }) { const [query, setQuery] = useState('') - 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 - } + const { members } = props // 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 87ac1501..2523b789 100644 --- a/web/pages/groups.tsx +++ b/web/pages/groups.tsx @@ -79,11 +79,7 @@ export default function Groups(props: { ) const matchesOrderedByRecentActivity = sortBy(groups, [ - (group) => - -1 * - (group.mostRecentChatActivityTime ?? - group.mostRecentContractAddedTime ?? - group.mostRecentActivityTime), + (group) => -1 * group.mostRecentActivityTime, ]).filter( (g) => checkAgainstQuery(query, g.name) ||