Comment on a separate subcollection

This commit is contained in:
Austin Chen 2022-01-03 23:17:54 -08:00
parent e4f3e3471b
commit 5e060919bb
4 changed files with 97 additions and 45 deletions

View File

@ -13,7 +13,8 @@ import {
XIcon,
} from '@heroicons/react/solid'
import { useBets } from '../hooks/use-bets'
import { Bet, createComment } from '../lib/firebase/bets'
import { Bet } from '../lib/firebase/bets'
import { Comment, mapCommentsByBetId } from '../lib/firebase/comments'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { OutcomeLabel } from './outcome-label'
@ -21,6 +22,8 @@ import { Contract, setContract } from '../lib/firebase/contracts'
import { useUser } from '../hooks/use-user'
import { Linkify } from './linkify'
import { Row } from './layout/row'
import { createComment } from '../lib/firebase/comments'
import { useComments } from '../hooks/use-comments'
dayjs.extend(relativeTime)
function FeedComment(props: { activityItem: any }) {
@ -289,7 +292,7 @@ function toFeedBet(bet: Bet) {
}
}
function toComment(bet: Bet) {
function toFeedComment(bet: Bet, comment: Comment) {
return {
id: bet.id,
contractId: bet.contractId,
@ -301,25 +304,22 @@ function toComment(bet: Bet) {
date: dayjs(bet.createdTime).fromNow(),
// Invariant: bet.comment exists
text: bet.comment!.text,
text: comment.text,
person: {
href: `/${bet.comment!.userUsername}`,
name: bet.comment!.userName,
avatarUrl: bet.comment!.userAvatarUrl,
href: `/${comment.userUsername}`,
name: comment.userName,
avatarUrl: comment.userAvatarUrl,
},
}
}
function toActivityItem(bet: Bet) {
return bet.comment ? toComment(bet) : toFeedBet(bet)
}
// Group together bets that are:
// - Within 24h of the first in the group
// - Do not have a comment
// - Were not created by this user
// Return a list of ActivityItems
function group(bets: Bet[], userId?: string) {
function group(bets: Bet[], comments: Comment[], userId?: string) {
const commentsMap = mapCommentsByBetId(comments)
const items: any[] = []
let group: Bet[] = []
@ -328,15 +328,20 @@ function group(bets: Bet[], userId?: string) {
if (group.length == 1) {
items.push(toActivityItem(group[0]))
} else if (group.length > 1) {
items.push({ type: 'betgroup', bets: [...group] })
items.push({ type: 'betgroup', bets: [...group], id: group[0].id })
}
group = []
}
function toActivityItem(bet: Bet) {
const comment = commentsMap[bet.id]
return comment ? toFeedComment(bet, comment) : toFeedBet(bet)
}
for (const bet of bets) {
const isCreator = userId === bet.userId
if (bet.comment || isCreator) {
if (commentsMap[bet.id] || isCreator) {
pushGroup()
// Create a single item for this
items.push(toActivityItem(bet))
@ -417,7 +422,13 @@ export function ContractFeed(props: { contract: Contract }) {
let bets = useBets(id)
if (bets === 'loading') bets = []
const allItems = [{ type: 'start', id: 0 }, ...group(bets, user?.id)]
let comments = useComments(id)
if (comments === 'loading') comments = []
const allItems = [
{ type: 'start', id: 0 },
...group(bets, comments, user?.id),
]
if (contract.closeTime) {
allItems.push({ type: 'close', id: `${contract.closeTime}` })
}

12
web/hooks/use-comments.ts Normal file
View File

@ -0,0 +1,12 @@
import { useEffect, useState } from 'react'
import { Comment, listenForComments } from '../lib/firebase/comments'
export const useComments = (contractId: string) => {
const [comments, setComments] = useState<Comment[] | 'loading'>('loading')
useEffect(() => {
if (contractId) return listenForComments(contractId, setComments)
}, [contractId])
return comments
}

View File

@ -4,11 +4,8 @@ import {
query,
onSnapshot,
where,
doc,
updateDoc,
} from 'firebase/firestore'
import { db } from './init'
import { User } from './users'
export type Bet = {
id: string
@ -31,16 +28,6 @@ export type Bet = {
isSold?: boolean // true if this BUY bet has been sold
createdTime: number
// Currently, comments are created after the bet, not atomically with the bet.
comment?: {
text: string
createdTime: number
// Denormalized, for rendering comments
userName?: string
userUsername?: string
userAvatarUrl?: string
}
}
function getBetsCollection(contractId: string) {
@ -75,21 +62,3 @@ export function listenForUserBets(
setBets(bets)
})
}
export async function createComment(
contractId: string,
betId: string,
text: string,
commenter: User
) {
const betRef = doc(getBetsCollection(contractId), betId)
return await updateDoc(betRef, {
comment: {
text: text,
createdTime: Date.now(),
userName: commenter.name,
userUsername: commenter.username,
userAvatarUrl: commenter.avatarUrl,
},
})
}

View File

@ -0,0 +1,60 @@
import { doc, collection, onSnapshot, setDoc } from 'firebase/firestore'
import { db } from './init'
import { User } from './users'
// Currently, comments are created after the bet, not atomically with the bet.
// They're uniquely identified by the pair contractId/betId.
export type Comment = {
contractId: string
betId: string
text: string
createdTime: number
// Denormalized, for rendering comments
userName?: string
userUsername?: string
userAvatarUrl?: string
}
export async function createComment(
contractId: string,
betId: string,
text: string,
commenter: User
) {
const ref = doc(getCommentsCollection(contractId), betId)
return await setDoc(ref, {
contractId,
betId,
text,
createdTime: Date.now(),
userName: commenter.name,
userUsername: commenter.username,
userAvatarUrl: commenter.avatarUrl,
})
}
function getCommentsCollection(contractId: string) {
return collection(db, 'contracts', contractId, 'comments')
}
export function listenForComments(
contractId: string,
setComments: (comments: Comment[]) => void
) {
return onSnapshot(getCommentsCollection(contractId), (snap) => {
const comments = snap.docs.map((doc) => doc.data() as Comment)
comments.sort((c1, c2) => c1.createdTime - c2.createdTime)
setComments(comments)
})
}
// Return a map of betId -> comment
export function mapCommentsByBetId(comments: Comment[]) {
const map: Record<string, Comment> = {}
for (const comment of comments) {
map[comment.betId] = comment
}
return map
}