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) ||