diff --git a/common/comment.ts b/common/comment.ts index c1b721cc..c7f9b855 100644 --- a/common/comment.ts +++ b/common/comment.ts @@ -1,15 +1,11 @@ import type { JSONContent } from '@tiptap/core' +export type AnyCommentType = OnContract | OnGroup + // Currently, comments are created after the bet, not atomically with the bet. // They're uniquely identified by the pair contractId/betId. -export type Comment = { +export type Comment = { id: string - commentType: 'contract' | 'group' - - contractId?: string - groupId?: string - betId?: string - answerOutcome?: string replyToCommentId?: string userId: string @@ -22,6 +18,21 @@ export type Comment = { userName: string userUsername: string userAvatarUrl?: string - contractSlug?: string - contractQuestion?: string +} & T + +type OnContract = { + commentType: 'contract' + contractId: string + contractSlug: string + contractQuestion: string + answerOutcome?: string + betId?: string } + +type OnGroup = { + commentType: 'group' + groupId: string +} + +export type ContractComment = Comment +export type GroupComment = Comment diff --git a/functions/src/on-create-comment-on-contract.ts b/functions/src/on-create-comment-on-contract.ts index 3fa0983d..9f19dfcc 100644 --- a/functions/src/on-create-comment-on-contract.ts +++ b/functions/src/on-create-comment-on-contract.ts @@ -2,7 +2,7 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' import { compact, uniq } from 'lodash' import { getContract, getUser, getValues } from './utils' -import { Comment } from '../../common/comment' +import { ContractComment } from '../../common/comment' import { sendNewCommentEmail } from './emails' import { Bet } from '../../common/bet' import { Answer } from '../../common/answer' @@ -29,7 +29,7 @@ export const onCreateCommentOnContract = functions contractQuestion: contract.question, }) - const comment = change.data() as Comment + const comment = change.data() as ContractComment const lastCommentTime = comment.createdTime const commentCreator = await getUser(comment.userId) @@ -64,7 +64,7 @@ export const onCreateCommentOnContract = functions : undefined } - const comments = await getValues( + const comments = await getValues( firestore.collection('contracts').doc(contractId).collection('comments') ) const relatedSourceType = comment.replyToCommentId diff --git a/functions/src/on-create-comment-on-group.ts b/functions/src/on-create-comment-on-group.ts index 0064480f..15f2bbc1 100644 --- a/functions/src/on-create-comment-on-group.ts +++ b/functions/src/on-create-comment-on-group.ts @@ -1,5 +1,5 @@ import * as functions from 'firebase-functions' -import { Comment } from '../../common/comment' +import { GroupComment } from '../../common/comment' import * as admin from 'firebase-admin' import { Group } from '../../common/group' import { User } from '../../common/user' @@ -14,7 +14,7 @@ export const onCreateCommentOnGroup = functions.firestore groupId: string } - const comment = change.data() as Comment + const comment = change.data() as GroupComment const creatorSnapshot = await firestore .collection('users') .doc(comment.userId) diff --git a/web/components/comments-list.tsx b/web/components/comments-list.tsx index 94799f4e..280787dd 100644 --- a/web/components/comments-list.tsx +++ b/web/components/comments-list.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react' -import { Comment } from 'common/comment' +import { Comment, ContractComment } from 'common/comment' import { groupConsecutive } from 'common/util/array' import { getUsersComments } from 'web/lib/firebase/comments' import { SiteLink } from './site-link' @@ -16,12 +16,6 @@ import { LoadingIndicator } from './loading-indicator' const COMMENTS_PER_PAGE = 50 -type ContractComment = Comment & { - contractId: string - contractSlug: string - contractQuestion: string -} - function contractPath(slug: string) { // by convention this includes the contract creator username, but we don't // have that handy, so we just put /market/ @@ -38,7 +32,9 @@ export function UserCommentsList(props: { user: User }) { useEffect(() => { getUsersComments(user.id).then((cs) => { // we don't show comments in groups here atm, just comments on contracts - setComments(cs.filter((c) => c.contractId) as ContractComment[]) + setComments( + cs.filter((c) => c.commentType == 'contract') as ContractComment[] + ) }) }, [user.id]) diff --git a/web/components/contract/contract-leaderboard.tsx b/web/components/contract/contract-leaderboard.tsx index 6f1a778d..22175876 100644 --- a/web/components/contract/contract-leaderboard.tsx +++ b/web/components/contract/contract-leaderboard.tsx @@ -1,5 +1,5 @@ import { Bet } from 'common/bet' -import { Comment } from 'common/comment' +import { ContractComment } from 'common/comment' import { resolvedPayout } from 'common/calculate' import { Contract } from 'common/contract' import { formatMoney } from 'common/util/format' @@ -65,7 +65,7 @@ export function ContractLeaderboard(props: { export function ContractTopTrades(props: { contract: Contract bets: Bet[] - comments: Comment[] + comments: ContractComment[] tips: CommentTipMap }) { const { contract, bets, comments, tips } = props diff --git a/web/components/contract/contract-tabs.tsx b/web/components/contract/contract-tabs.tsx index eb455df0..9e9f62bf 100644 --- a/web/components/contract/contract-tabs.tsx +++ b/web/components/contract/contract-tabs.tsx @@ -1,6 +1,6 @@ import { Bet } from 'common/bet' import { Contract } from 'common/contract' -import { Comment } from 'web/lib/firebase/comments' +import { ContractComment } from 'common/comment' import { User } from 'common/user' import { ContractActivity } from '../feed/contract-activity' import { ContractBetsTable, BetsSummary } from '../bets-list' @@ -15,7 +15,7 @@ export function ContractTabs(props: { contract: Contract user: User | null | undefined bets: Bet[] - comments: Comment[] + comments: ContractComment[] tips: CommentTipMap }) { const { contract, user, bets, tips } = props diff --git a/web/components/feed/activity-items.ts b/web/components/feed/activity-items.ts index 511767c6..bcbb6721 100644 --- a/web/components/feed/activity-items.ts +++ b/web/components/feed/activity-items.ts @@ -3,7 +3,7 @@ import { uniq, sortBy } from 'lodash' import { Answer } from 'common/answer' import { Bet } from 'common/bet' import { getOutcomeProbability } from 'common/calculate' -import { Comment } from 'common/comment' +import { ContractComment } from 'common/comment' import { Contract, FreeResponseContract } from 'common/contract' import { User } from 'common/user' import { CommentTipMap } from 'web/hooks/use-tip-txns' @@ -28,7 +28,7 @@ type BaseActivityItem = { export type CommentInputItem = BaseActivityItem & { type: 'commentInput' betsByCurrentUser: Bet[] - commentsByCurrentUser: Comment[] + commentsByCurrentUser: ContractComment[] } export type DescriptionItem = BaseActivityItem & { @@ -50,8 +50,8 @@ export type BetItem = BaseActivityItem & { export type CommentThreadItem = BaseActivityItem & { type: 'commentThread' - parentComment: Comment - comments: Comment[] + parentComment: ContractComment + comments: ContractComment[] tips: CommentTipMap bets: Bet[] } @@ -60,7 +60,7 @@ export type AnswerGroupItem = BaseActivityItem & { type: 'answergroup' user: User | undefined | null answer: Answer - comments: Comment[] + comments: ContractComment[] tips: CommentTipMap bets: Bet[] } @@ -84,7 +84,7 @@ export type LiquidityItem = BaseActivityItem & { function getAnswerAndCommentInputGroups( contract: FreeResponseContract, bets: Bet[], - comments: Comment[], + comments: ContractComment[], tips: CommentTipMap, user: User | undefined | null ) { @@ -116,7 +116,7 @@ function getAnswerAndCommentInputGroups( function getCommentThreads( bets: Bet[], - comments: Comment[], + comments: ContractComment[], tips: CommentTipMap, contract: Contract ) { @@ -135,7 +135,7 @@ function getCommentThreads( return items } -function commentIsGeneralComment(comment: Comment, contract: Contract) { +function commentIsGeneralComment(comment: ContractComment, contract: Contract) { return ( comment.answerOutcome === undefined && (contract.outcomeType === 'FREE_RESPONSE' @@ -147,7 +147,7 @@ function commentIsGeneralComment(comment: Comment, contract: Contract) { export function getSpecificContractActivityItems( contract: Contract, bets: Bet[], - comments: Comment[], + comments: ContractComment[], liquidityProvisions: LiquidityProvision[], tips: CommentTipMap, user: User | null | undefined, diff --git a/web/components/feed/contract-activity.tsx b/web/components/feed/contract-activity.tsx index cd490701..3cc0acb0 100644 --- a/web/components/feed/contract-activity.tsx +++ b/web/components/feed/contract-activity.tsx @@ -1,5 +1,5 @@ import { Contract } from 'web/lib/firebase/contracts' -import { Comment } from 'web/lib/firebase/comments' +import { ContractComment } from 'common/comment' import { Bet } from 'common/bet' import { useBets } from 'web/hooks/use-bets' import { getSpecificContractActivityItems } from './activity-items' @@ -12,7 +12,7 @@ import { LiquidityProvision } from 'common/liquidity-provision' export function ContractActivity(props: { contract: Contract bets: Bet[] - comments: Comment[] + comments: ContractComment[] liquidityProvisions: LiquidityProvision[] tips: CommentTipMap user: User | null | undefined diff --git a/web/components/feed/feed-answer-comment-group.tsx b/web/components/feed/feed-answer-comment-group.tsx index edaf1fe5..86686f1f 100644 --- a/web/components/feed/feed-answer-comment-group.tsx +++ b/web/components/feed/feed-answer-comment-group.tsx @@ -1,6 +1,6 @@ import { Answer } from 'common/answer' import { Bet } from 'common/bet' -import { Comment } from 'common/comment' +import { ContractComment } from 'common/comment' import React, { useEffect, useState } from 'react' import { Col } from 'web/components/layout/col' import { Row } from 'web/components/layout/row' @@ -24,7 +24,7 @@ export function FeedAnswerCommentGroup(props: { contract: any user: User | undefined | null answer: Answer - comments: Comment[] + comments: ContractComment[] tips: CommentTipMap bets: Bet[] }) { @@ -69,7 +69,7 @@ export function FeedAnswerCommentGroup(props: { ]) const scrollAndOpenReplyInput = useEvent( - (comment?: Comment, answer?: Answer) => { + (comment?: ContractComment, answer?: Answer) => { setReplyToUser( comment ? { id: comment.userId, username: comment.userUsername } diff --git a/web/components/feed/feed-comments.tsx b/web/components/feed/feed-comments.tsx index d4ba98b6..0541a7ba 100644 --- a/web/components/feed/feed-comments.tsx +++ b/web/components/feed/feed-comments.tsx @@ -1,5 +1,5 @@ import { Bet } from 'common/bet' -import { Comment } from 'common/comment' +import { ContractComment } from 'common/comment' import { User } from 'common/user' import { Contract } from 'common/contract' import React, { useEffect, useState } from 'react' @@ -32,9 +32,9 @@ import { Editor } from '@tiptap/react' export function FeedCommentThread(props: { contract: Contract - comments: Comment[] + comments: ContractComment[] tips: CommentTipMap - parentComment: Comment + parentComment: ContractComment bets: Bet[] smallAvatar?: boolean }) { @@ -50,7 +50,7 @@ export function FeedCommentThread(props: { ) commentsList.unshift(parentComment) - function scrollAndOpenReplyInput(comment: Comment) { + function scrollAndOpenReplyInput(comment: ContractComment) { setReplyToUser({ id: comment.userId, username: comment.userUsername }) setShowReply(true) } @@ -95,10 +95,10 @@ export function FeedCommentThread(props: { export function CommentRepliesList(props: { contract: Contract - commentsList: Comment[] + commentsList: ContractComment[] betsByUserId: Dictionary tips: CommentTipMap - scrollAndOpenReplyInput: (comment: Comment) => void + scrollAndOpenReplyInput: (comment: ContractComment) => void bets: Bet[] treatFirstIndexEqually?: boolean smallAvatar?: boolean @@ -156,12 +156,12 @@ export function CommentRepliesList(props: { export function FeedComment(props: { contract: Contract - comment: Comment + comment: ContractComment tips: CommentTips betsBySameUser: Bet[] probAtCreatedTime?: number smallAvatar?: boolean - onReplyClick?: (comment: Comment) => void + onReplyClick?: (comment: ContractComment) => void }) { const { contract, @@ -274,7 +274,7 @@ export function FeedComment(props: { export function getMostRecentCommentableBet( betsByCurrentUser: Bet[], - commentsByCurrentUser: Comment[], + commentsByCurrentUser: ContractComment[], user?: User | null, answerOutcome?: string ) { @@ -319,7 +319,7 @@ function CommentStatus(props: { export function CommentInput(props: { contract: Contract betsByCurrentUser: Bet[] - commentsByCurrentUser: Comment[] + commentsByCurrentUser: ContractComment[] replyToUser?: { id: string; username: string } // Reply to a free response answer parentAnswerOutcome?: string diff --git a/web/components/feed/find-active-contracts.ts b/web/components/feed/find-active-contracts.ts index c53b3622..ad2af970 100644 --- a/web/components/feed/find-active-contracts.ts +++ b/web/components/feed/find-active-contracts.ts @@ -1,6 +1,6 @@ import { groupBy, mapValues, maxBy, sortBy } from 'lodash' import { Contract } from 'web/lib/firebase/contracts' -import { Comment } from 'web/lib/firebase/comments' +import { ContractComment } from 'common/comment' import { Bet } from 'common/bet' const MAX_ACTIVE_CONTRACTS = 75 @@ -19,7 +19,7 @@ function lastActivityTime(contract: Contract) { // - Bet on market export function findActiveContracts( allContracts: Contract[], - recentComments: Comment[], + recentComments: ContractComment[], recentBets: Bet[], seenContracts: { [contractId: string]: number } ) { @@ -73,7 +73,7 @@ export function findActiveContracts( ) const contractMostRecentComment = mapValues( contractComments, - (comments) => maxBy(comments, (c) => c.createdTime) as Comment + (comments) => maxBy(comments, (c) => c.createdTime) as ContractComment ) const prioritizedContracts = sortBy(activeContracts, (c) => { diff --git a/web/components/groups/group-chat.tsx b/web/components/groups/group-chat.tsx index a37377a5..781705c2 100644 --- a/web/components/groups/group-chat.tsx +++ b/web/components/groups/group-chat.tsx @@ -4,7 +4,8 @@ import { PrivateUser, User } from 'common/user' import React, { useEffect, memo, useState, useMemo } from 'react' import { Avatar } from 'web/components/avatar' import { Group } from 'common/group' -import { Comment, createCommentOnGroup } from 'web/lib/firebase/comments' +import { Comment, GroupComment } from 'common/comment' +import { createCommentOnGroup } from 'web/lib/firebase/comments' import { CommentInputTextArea } from 'web/components/feed/feed-comments' import { track } from 'web/lib/service/analytics' import { firebaseLogin } from 'web/lib/firebase/users' @@ -24,7 +25,7 @@ import { setNotificationsAsSeen } from 'web/pages/notifications' import { usePrivateUser } from 'web/hooks/use-user' export function GroupChat(props: { - messages: Comment[] + messages: GroupComment[] user: User | null | undefined group: Group tips: CommentTipMap @@ -58,7 +59,7 @@ export function GroupChat(props: { // array of groups, where each group is an array of messages that are displayed as one const groupedMessages = useMemo(() => { // Group messages with createdTime within 2 minutes of each other. - const tempGrouped: Comment[][] = [] + const tempGrouped: GroupComment[][] = [] for (let i = 0; i < messages.length; i++) { const message = messages[i] if (i === 0) tempGrouped.push([message]) @@ -193,7 +194,7 @@ export function GroupChat(props: { } export function GroupChatInBubble(props: { - messages: Comment[] + messages: GroupComment[] user: User | null | undefined privateUser: PrivateUser | null | undefined group: Group @@ -309,7 +310,7 @@ function GroupChatNotificationsIcon(props: { const GroupMessage = memo(function GroupMessage_(props: { user: User | null | undefined - comments: Comment[] + comments: GroupComment[] group: Group onReplyClick?: (comment: Comment) => void setRef?: (ref: HTMLDivElement) => void diff --git a/web/components/tipper.tsx b/web/components/tipper.tsx index dbacbee9..b9ebdefc 100644 --- a/web/components/tipper.tsx +++ b/web/components/tipper.tsx @@ -42,6 +42,10 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { return } + const contractId = + comment.commentType === 'contract' ? comment.contractId : undefined + const groupId = + comment.commentType === 'group' ? comment.groupId : undefined await transact({ amount: change, fromId: user.id, @@ -50,18 +54,14 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) { toType: 'USER', token: 'M$', category: 'TIP', - data: { - contractId: comment.contractId, - commentId: comment.id, - groupId: comment.groupId, - }, + data: { commentId: comment.id, contractId, groupId }, description: `${user.name} tipped M$ ${change} to ${comment.userName} for a comment`, }) track('send comment tip', { - contractId: comment.contractId, commentId: comment.id, - groupId: comment.groupId, + contractId, + groupId, amount: change, fromId: user.id, toId: comment.userId, diff --git a/web/hooks/use-comments.ts b/web/hooks/use-comments.ts index 7d644444..172d2cee 100644 --- a/web/hooks/use-comments.ts +++ b/web/hooks/use-comments.ts @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react' +import { Comment, ContractComment, GroupComment } from 'common/comment' import { - Comment, listenForCommentsOnContract, listenForCommentsOnGroup, listenForRecentComments, } from 'web/lib/firebase/comments' export const useComments = (contractId: string) => { - const [comments, setComments] = useState() + const [comments, setComments] = useState() useEffect(() => { if (contractId) return listenForCommentsOnContract(contractId, setComments) @@ -16,7 +16,7 @@ export const useComments = (contractId: string) => { return comments } export const useCommentsOnGroup = (groupId: string | undefined) => { - const [comments, setComments] = useState() + const [comments, setComments] = useState() useEffect(() => { if (groupId) return listenForCommentsOnGroup(groupId, setComments) diff --git a/web/lib/firebase/comments.ts b/web/lib/firebase/comments.ts index f0678367..f7c947fe 100644 --- a/web/lib/firebase/comments.ts +++ b/web/lib/firebase/comments.ts @@ -11,7 +11,7 @@ import { import { getValues, listenForValues } from './utils' import { db } from './init' import { User } from 'common/user' -import { Comment } from 'common/comment' +import { Comment, ContractComment, GroupComment } from 'common/comment' import { removeUndefinedProps } from 'common/util/object' import { track } from '@amplitude/analytics-browser' import { JSONContent } from '@tiptap/react' @@ -31,7 +31,8 @@ export async function createCommentOnContract( const ref = betId ? doc(getCommentsCollection(contractId), betId) : doc(getCommentsCollection(contractId)) - const comment: Comment = removeUndefinedProps({ + // contract slug and question are set via trigger + const comment = removeUndefinedProps({ id: ref.id, commentType: 'contract', contractId, @@ -60,7 +61,7 @@ export async function createCommentOnGroup( replyToCommentId?: string ) { const ref = doc(getCommentsOnGroupCollection(groupId)) - const comment: Comment = removeUndefinedProps({ + const comment = removeUndefinedProps({ id: ref.id, commentType: 'group', groupId, @@ -96,7 +97,7 @@ export async function listAllComments(contractId: string) { } export async function listAllCommentsOnGroup(groupId: string) { - const comments = await getValues( + const comments = await getValues( getCommentsOnGroupCollection(groupId) ) comments.sort((c1, c2) => c1.createdTime - c2.createdTime) @@ -105,9 +106,9 @@ export async function listAllCommentsOnGroup(groupId: string) { export function listenForCommentsOnContract( contractId: string, - setComments: (comments: Comment[]) => void + setComments: (comments: ContractComment[]) => void ) { - return listenForValues( + return listenForValues( getCommentsCollection(contractId), (comments) => { comments.sort((c1, c2) => c1.createdTime - c2.createdTime) @@ -117,9 +118,9 @@ export function listenForCommentsOnContract( } export function listenForCommentsOnGroup( groupId: string, - setComments: (comments: Comment[]) => void + setComments: (comments: GroupComment[]) => void ) { - return listenForValues( + return listenForValues( getCommentsOnGroupCollection(groupId), (comments) => { comments.sort((c1, c2) => c1.createdTime - c2.createdTime) diff --git a/web/pages/[username]/[contractSlug].tsx b/web/pages/[username]/[contractSlug].tsx index 94773b6d..41ad5957 100644 --- a/web/pages/[username]/[contractSlug].tsx +++ b/web/pages/[username]/[contractSlug].tsx @@ -17,7 +17,7 @@ import { import { SEO } from 'web/components/SEO' import { Page } from 'web/components/page' import { Bet, listAllBets } from 'web/lib/firebase/bets' -import { Comment, listAllComments } from 'web/lib/firebase/comments' +import { listAllComments } from 'web/lib/firebase/comments' import Custom404 from '../404' import { AnswersPanel } from 'web/components/answers/answers-panel' import { fromPropz, usePropz } from 'web/hooks/use-propz' @@ -38,6 +38,7 @@ import { CommentTipMap, useTipTxns } from 'web/hooks/use-tip-txns' import { useSaveReferral } from 'web/hooks/use-save-referral' import { getOpenGraphProps } from 'web/components/contract/contract-card-preview' import { User } from 'common/user' +import { ContractComment } from 'common/comment' import { listUsers } from 'web/lib/firebase/users' import { FeedComment } from 'web/components/feed/feed-comments' import { Title } from 'web/components/title' @@ -78,7 +79,7 @@ export default function ContractPage(props: { contract: Contract | null username: string bets: Bet[] - comments: Comment[] + comments: ContractComment[] slug: string backToHome?: () => void }) { @@ -314,7 +315,7 @@ function ContractLeaderboard(props: { contract: Contract; bets: Bet[] }) { function ContractTopTrades(props: { contract: Contract bets: Bet[] - comments: Comment[] + comments: ContractComment[] tips: CommentTipMap }) { const { contract, bets, comments, tips } = props diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index c5255974..2ee9fa49 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -46,7 +46,7 @@ import { ENV_CONFIG } from 'common/envs/constants' import { useSaveReferral } from 'web/hooks/use-save-referral' import { Button } from 'web/components/button' import { listAllCommentsOnGroup } from 'web/lib/firebase/comments' -import { Comment } from 'common/comment' +import { GroupComment } from 'common/comment' import { GroupChat } from 'web/components/groups/group-chat' export const getStaticProps = fromPropz(getStaticPropz) @@ -123,7 +123,7 @@ export default function GroupPage(props: { topTraders: User[] creatorScores: { [userId: string]: number } topCreators: User[] - messages: Comment[] + messages: GroupComment[] }) { props = usePropz(props, getStaticPropz) ?? { group: null,