Comment on a separate subcollection
This commit is contained in:
parent
e4f3e3471b
commit
5e060919bb
|
@ -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
12
web/hooks/use-comments.ts
Normal 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
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
60
web/lib/firebase/comments.ts
Normal file
60
web/lib/firebase/comments.ts
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user