Compare commits

...

1 Commits

Author SHA1 Message Date
Austin Chen
3b575080b0 Allow editing of comments on a market 2022-08-20 13:04:50 -07:00
3 changed files with 77 additions and 29 deletions

View File

@ -90,9 +90,11 @@ service cloud.firestore {
.hasOnly(['description', 'closeTime', 'question']) .hasOnly(['description', 'closeTime', 'question'])
&& resource.data.creatorId == request.auth.uid; && resource.data.creatorId == request.auth.uid;
allow update: if isAdmin(); allow update: if isAdmin();
match /comments/{commentId} {
allow create: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data); // TODO: This runs afoul of FirebaseError: Missing or insufficient permissions
} match /comments/{commentId} {
allow create, update: if request.auth != null && commentMatchesUser(request.auth.uid, request.resource.data);
}
} }
match /{somePath=**}/bets/{betId} { match /{somePath=**}/bets/{betId} {

View File

@ -16,6 +16,7 @@ import { CopyLinkDateTimeComponent } from 'web/components/feed/copy-link-date-ti
import { firebaseLogin } from 'web/lib/firebase/users' import { firebaseLogin } from 'web/lib/firebase/users'
import { import {
createCommentOnContract, createCommentOnContract,
editCommentOnContract,
MAX_COMMENT_LENGTH, MAX_COMMENT_LENGTH,
} from 'web/lib/firebase/comments' } from 'web/lib/firebase/comments'
import { BetStatusText } from 'web/components/feed/feed-bets' import { BetStatusText } from 'web/components/feed/feed-bets'
@ -28,7 +29,7 @@ import { Tipper } from '../tipper'
import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns' import { CommentTipMap, CommentTips } from 'web/hooks/use-tip-txns'
import { useWindowSize } from 'web/hooks/use-window-size' import { useWindowSize } from 'web/hooks/use-window-size'
import { Content, TextEditor, useTextEditor } from '../editor' import { Content, TextEditor, useTextEditor } from '../editor'
import { Editor } from '@tiptap/react' import { Editor, JSONContent } from '@tiptap/react'
export function FeedCommentThread(props: { export function FeedCommentThread(props: {
contract: Contract contract: Contract
@ -113,6 +114,8 @@ export function CommentRepliesList(props: {
scrollAndOpenReplyInput, scrollAndOpenReplyInput,
treatFirstIndexEqually, treatFirstIndexEqually,
} = props } = props
const [editCommentId, setEditCommentId] = useState<string | undefined>()
const user = useUser()
return ( return (
<> <>
{commentsList.map((comment, commentIdx) => ( {commentsList.map((comment, commentIdx) => (
@ -131,23 +134,36 @@ export function CommentRepliesList(props: {
aria-hidden="true" aria-hidden="true"
/> />
)} )}
<FeedComment {editCommentId === comment.id ? (
contract={contract} <CommentInput
comment={comment} contract={contract}
tips={tips[comment.id]} // TODO: These were Copilot-generated, examine more closely
betsBySameUser={betsByUserId[comment.userId] ?? []} betsByCurrentUser={(user && betsByUserId[user.id]) ?? []}
onReplyClick={scrollAndOpenReplyInput} commentsByCurrentUser={commentsList.filter(
probAtCreatedTime={ (c) => c.userId === user?.id
contract.outcomeType === 'BINARY' )}
? minBy(bets, (bet) => { toEdit={comment}
return bet.createdTime < comment.createdTime />
? comment.createdTime - bet.createdTime ) : (
: comment.createdTime <FeedComment
})?.probAfter contract={contract}
: undefined comment={comment}
} tips={tips[comment.id]}
smallAvatar={smallAvatar} betsBySameUser={betsByUserId[comment.userId] ?? []}
/> onReplyClick={scrollAndOpenReplyInput}
onEditClick={() => setEditCommentId(comment.id)}
probAtCreatedTime={
contract.outcomeType === 'BINARY'
? minBy(bets, (bet) => {
return bet.createdTime < comment.createdTime
? comment.createdTime - bet.createdTime
: comment.createdTime
})?.probAfter
: undefined
}
smallAvatar={smallAvatar}
/>
)}
</div> </div>
))} ))}
</> </>
@ -162,6 +178,7 @@ export function FeedComment(props: {
probAtCreatedTime?: number probAtCreatedTime?: number
smallAvatar?: boolean smallAvatar?: boolean
onReplyClick?: (comment: ContractComment) => void onReplyClick?: (comment: ContractComment) => void
onEditClick?: (comment: ContractComment) => void
}) { }) {
const { const {
contract, contract,
@ -170,6 +187,7 @@ export function FeedComment(props: {
betsBySameUser, betsBySameUser,
probAtCreatedTime, probAtCreatedTime,
onReplyClick, onReplyClick,
onEditClick,
} = props } = props
const { text, content, userUsername, userName, userAvatarUrl, createdTime } = const { text, content, userUsername, userName, userAvatarUrl, createdTime } =
comment comment
@ -199,6 +217,9 @@ export function FeedComment(props: {
matchedBet ? [] : betsBySameUser matchedBet ? [] : betsBySameUser
) )
const self = useUser()
const canEdit = self?.id === comment.userId
return ( return (
<Row <Row
className={clsx( className={clsx(
@ -266,6 +287,14 @@ export function FeedComment(props: {
Reply Reply
</button> </button>
)} )}
{canEdit && onEditClick ? (
<button
className="font-bold hover:underline"
onClick={() => onEditClick(comment)}
>
Edit
</button>
) : null}
</Row> </Row>
</div> </div>
</Row> </Row>
@ -326,6 +355,7 @@ export function CommentInput(props: {
// Reply to another comment // Reply to another comment
parentCommentId?: string parentCommentId?: string
onSubmitComment?: () => void onSubmitComment?: () => void
toEdit?: ContractComment
}) { }) {
const { const {
contract, contract,
@ -335,6 +365,7 @@ export function CommentInput(props: {
parentCommentId, parentCommentId,
replyToUser, replyToUser,
onSubmitComment, onSubmitComment,
toEdit,
} = props } = props
const user = useUser() const user = useUser()
const { editor, upload } = useTextEditor({ const { editor, upload } = useTextEditor({
@ -344,6 +375,7 @@ export function CommentInput(props: {
!!parentCommentId || !!parentAnswerOutcome !!parentCommentId || !!parentAnswerOutcome
? 'Write a reply...' ? 'Write a reply...'
: 'Write a comment...', : 'Write a comment...',
defaultValue: toEdit?.content,
}) })
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -362,14 +394,19 @@ export function CommentInput(props: {
} }
if (!editor || editor.isEmpty || isSubmitting) return if (!editor || editor.isEmpty || isSubmitting) return
setIsSubmitting(true) setIsSubmitting(true)
await createCommentOnContract( if (toEdit) {
contract.id, await editCommentOnContract(toEdit, editor.getJSON())
editor.getJSON(), } else {
user, await createCommentOnContract(
betId, contract.id,
parentAnswerOutcome, editor.getJSON(),
parentCommentId user,
) betId,
parentAnswerOutcome,
parentCommentId
)
}
onSubmitComment?.() onSubmitComment?.()
setIsSubmitting(false) setIsSubmitting(false)
} }

View File

@ -20,6 +20,15 @@ export type { Comment }
export const MAX_COMMENT_LENGTH = 10000 export const MAX_COMMENT_LENGTH = 10000
export async function editCommentOnContract(
toEdit: ContractComment,
newContent: JSONContent
) {
const { id, contractId } = toEdit
const ref = doc(getCommentsCollection(contractId), id)
await setDoc(ref, { content: newContent })
}
export async function createCommentOnContract( export async function createCommentOnContract(
contractId: string, contractId: string,
content: JSONContent, content: JSONContent,