Comment on a separate subcollection
This commit is contained in:
parent
e4f3e3471b
commit
5e060919bb
|
@ -13,7 +13,8 @@ import {
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@heroicons/react/solid'
|
} from '@heroicons/react/solid'
|
||||||
import { useBets } from '../hooks/use-bets'
|
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 dayjs from 'dayjs'
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import { OutcomeLabel } from './outcome-label'
|
import { OutcomeLabel } from './outcome-label'
|
||||||
|
@ -21,6 +22,8 @@ import { Contract, setContract } from '../lib/firebase/contracts'
|
||||||
import { useUser } from '../hooks/use-user'
|
import { useUser } from '../hooks/use-user'
|
||||||
import { Linkify } from './linkify'
|
import { Linkify } from './linkify'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
|
import { createComment } from '../lib/firebase/comments'
|
||||||
|
import { useComments } from '../hooks/use-comments'
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
function FeedComment(props: { activityItem: any }) {
|
function FeedComment(props: { activityItem: any }) {
|
||||||
|
@ -289,7 +292,7 @@ function toFeedBet(bet: Bet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toComment(bet: Bet) {
|
function toFeedComment(bet: Bet, comment: Comment) {
|
||||||
return {
|
return {
|
||||||
id: bet.id,
|
id: bet.id,
|
||||||
contractId: bet.contractId,
|
contractId: bet.contractId,
|
||||||
|
@ -301,25 +304,22 @@ function toComment(bet: Bet) {
|
||||||
date: dayjs(bet.createdTime).fromNow(),
|
date: dayjs(bet.createdTime).fromNow(),
|
||||||
|
|
||||||
// Invariant: bet.comment exists
|
// Invariant: bet.comment exists
|
||||||
text: bet.comment!.text,
|
text: comment.text,
|
||||||
person: {
|
person: {
|
||||||
href: `/${bet.comment!.userUsername}`,
|
href: `/${comment.userUsername}`,
|
||||||
name: bet.comment!.userName,
|
name: comment.userName,
|
||||||
avatarUrl: bet.comment!.userAvatarUrl,
|
avatarUrl: comment.userAvatarUrl,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toActivityItem(bet: Bet) {
|
|
||||||
return bet.comment ? toComment(bet) : toFeedBet(bet)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group together bets that are:
|
// Group together bets that are:
|
||||||
// - Within 24h of the first in the group
|
// - Within 24h of the first in the group
|
||||||
// - Do not have a comment
|
// - Do not have a comment
|
||||||
// - Were not created by this user
|
// - Were not created by this user
|
||||||
// Return a list of ActivityItems
|
// 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[] = []
|
const items: any[] = []
|
||||||
let group: Bet[] = []
|
let group: Bet[] = []
|
||||||
|
|
||||||
|
@ -328,15 +328,20 @@ function group(bets: Bet[], userId?: string) {
|
||||||
if (group.length == 1) {
|
if (group.length == 1) {
|
||||||
items.push(toActivityItem(group[0]))
|
items.push(toActivityItem(group[0]))
|
||||||
} else if (group.length > 1) {
|
} else if (group.length > 1) {
|
||||||
items.push({ type: 'betgroup', bets: [...group] })
|
items.push({ type: 'betgroup', bets: [...group], id: group[0].id })
|
||||||
}
|
}
|
||||||
group = []
|
group = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toActivityItem(bet: Bet) {
|
||||||
|
const comment = commentsMap[bet.id]
|
||||||
|
return comment ? toFeedComment(bet, comment) : toFeedBet(bet)
|
||||||
|
}
|
||||||
|
|
||||||
for (const bet of bets) {
|
for (const bet of bets) {
|
||||||
const isCreator = userId === bet.userId
|
const isCreator = userId === bet.userId
|
||||||
|
|
||||||
if (bet.comment || isCreator) {
|
if (commentsMap[bet.id] || isCreator) {
|
||||||
pushGroup()
|
pushGroup()
|
||||||
// Create a single item for this
|
// Create a single item for this
|
||||||
items.push(toActivityItem(bet))
|
items.push(toActivityItem(bet))
|
||||||
|
@ -417,7 +422,13 @@ export function ContractFeed(props: { contract: Contract }) {
|
||||||
let bets = useBets(id)
|
let bets = useBets(id)
|
||||||
if (bets === 'loading') bets = []
|
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) {
|
if (contract.closeTime) {
|
||||||
allItems.push({ type: 'close', id: `${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,
|
query,
|
||||||
onSnapshot,
|
onSnapshot,
|
||||||
where,
|
where,
|
||||||
doc,
|
|
||||||
updateDoc,
|
|
||||||
} from 'firebase/firestore'
|
} from 'firebase/firestore'
|
||||||
import { db } from './init'
|
import { db } from './init'
|
||||||
import { User } from './users'
|
|
||||||
|
|
||||||
export type Bet = {
|
export type Bet = {
|
||||||
id: string
|
id: string
|
||||||
|
@ -31,16 +28,6 @@ export type Bet = {
|
||||||
isSold?: boolean // true if this BUY bet has been sold
|
isSold?: boolean // true if this BUY bet has been sold
|
||||||
|
|
||||||
createdTime: number
|
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) {
|
function getBetsCollection(contractId: string) {
|
||||||
|
@ -75,21 +62,3 @@ export function listenForUserBets(
|
||||||
setBets(bets)
|
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