diff --git a/packages/common/src/api/comments.ts b/packages/common/src/api/comments.ts index 730718b5f6e..0b0cba4e037 100644 --- a/packages/common/src/api/comments.ts +++ b/packages/common/src/api/comments.ts @@ -7,9 +7,26 @@ import { ID } from '~/models' import { decodeHashId, encodeHashId } from '~/utils' // Helper method to save on some copy-pasta +// Updates the array of all comments +const optimisticUpdateCommentList = ( + entityId: number, + updateRecipe: (prevState: Comment[] | undefined) => void, // Could also return Comment[] but its easier to modify the prevState proxy array directly + dispatch: ThunkDispatch +) => { + dispatch( + commentsApi.util.updateQueryData( + 'getCommentsByTrackId', + { entityId }, + updateRecipe + ) + ) +} + +// Helper method to save on some copy-pasta +// Updates a specific comment const optimisticUpdateComment = ( id: string, - updateRecipe: (prevState: Comment | undefined) => Comment, + updateRecipe: (prevState: Comment | undefined) => Comment | void, dispatch: ThunkDispatch ) => { dispatch( @@ -52,8 +69,7 @@ const commentsApi = createApi({ }, options: { type: 'query' } }, - - // Non-optimistic mutations + // Non-optimistically updated mutations (updates after confirmation) postComment: { async fetch( { parentCommentId, ...commentData }: CommentMetadata, @@ -69,22 +85,57 @@ const commentsApi = createApi({ return commentsRes }, options: { type: 'mutation' }, - async onQuerySuccess({ data: comment }, { entityId }, { dispatch }) { - dispatch( - commentsApi.util.updateQueryData( - 'getCommentsByTrackId', - { entityId }, - (prevState) => { - // TODO: how should we handle sorting here? - prevState?.unshift(comment) // add new comment to top of comment section + async onQuerySuccess( + { data: newId }, + { entityId, body, userId, timestampS, parentCommentId }, + { dispatch } + ) { + const newComment: Comment = { + id: newId, + userId, + message: body, + isPinned: false, + timestampS, + reactCount: 0, + replies: undefined, + createdAt: new Date().toISOString(), + updatedAt: undefined + } + optimisticUpdateCommentList( + entityId, + (prevState) => { + if (prevState) { + if (parentCommentId) { + const parentCommentIndex = prevState?.findIndex( + (comment) => comment.id === parentCommentId + ) + if (parentCommentIndex && parentCommentIndex >= 0) { + const parentComment = prevState[parentCommentIndex] + parentComment.replies = parentComment.replies || [] + parentComment.replies.push(newComment) + } + } else { + prevState.unshift(newComment) // add new comment to top of comment section + } } - ) + }, + dispatch + ) + optimisticUpdateComment( + parentCommentId ?? newId, + (parentComment) => { + if (parentCommentId && parentComment) { + parentComment.replies = parentComment.replies || [] + parentComment.replies.push(newComment) + return parentComment + } else { + return newComment + } + }, + dispatch ) - optimisticUpdateComment(comment.id, () => comment, dispatch) } }, - - // TODO: should this be optimistic or not? deleteCommentById: { async fetch( { id, userId, entityId }: { id: string; userId: ID; entityId: ID }, @@ -104,9 +155,23 @@ const commentsApi = createApi({ const sdk = await audiusSdk() await sdk.comments.deleteComment(commentData) }, - options: { type: 'mutation' } + options: { type: 'mutation' }, + onQuerySuccess(_res, { id, entityId }, { dispatch }) { + optimisticUpdateCommentList( + entityId, + (prevState) => { + const indexToRemove = prevState?.findIndex( + (comment: Comment) => comment.id === id + ) + if (indexToRemove && indexToRemove >= 0) { + prevState?.splice(indexToRemove, 1) + } + }, + dispatch + ) + } }, - // Optimistic mutations + // Optimistically updated mutations editCommentById: { async fetch( { diff --git a/packages/libs/src/sdk/api/comments/CommentsAPI.ts b/packages/libs/src/sdk/api/comments/CommentsAPI.ts index 0a0c93bb80c..14b83abfa08 100644 --- a/packages/libs/src/sdk/api/comments/CommentsAPI.ts +++ b/packages/libs/src/sdk/api/comments/CommentsAPI.ts @@ -44,12 +44,7 @@ export class CommentsApi extends GeneratedCommentsApi { auth: this.auth }) this.logger.info('Successfully posted a comment') - - // After posting a comment, fetch the comment to get the full object - const newCommentResponse = await this.getComment({ - id: newCommentId.toString() - }) - return newCommentResponse + return newCommentId } async editComment(metadata: CommentMetadata) { diff --git a/packages/web/src/components/comments/CommentBlock.tsx b/packages/web/src/components/comments/CommentBlock.tsx index 0a56aabd78d..094893d09e4 100644 --- a/packages/web/src/components/comments/CommentBlock.tsx +++ b/packages/web/src/components/comments/CommentBlock.tsx @@ -10,6 +10,7 @@ import { IconMerch, IconPencil, IconTrash, + LoadingSpinner, Text, TextLink } from '@audius/harmony' @@ -66,10 +67,7 @@ export type CommentBlockProps = { } export const CommentBlock = (props: CommentBlockProps) => { - const { - comment, - parentCommentId // Parent comment ID will only exist on replies - } = props + const { comment, parentCommentId } = props const { isPinned, message, @@ -89,11 +87,14 @@ export const CommentBlock = (props: CommentBlockProps) => { } = useCurrentCommentSection() const [editComment] = useEditComment() - const [deleteComment] = useDeleteComment() + const [deleteComment, { status: deleteCommentStatus }] = useDeleteComment() + const prevDeleteCommentStatus = usePrevious(deleteCommentStatus) const [reactToComment] = useReactToComment() const [pinComment] = usePinComment() + // Note: comment post status is shared across all inputs they may have open const [postComment, { status: commentPostStatus }] = usePostComment() const prevPostStatus = usePrevious(commentPostStatus) + const [isDeleting, setIsDeleting] = useState(false) useEffect(() => { if ( prevPostStatus !== commentPostStatus && @@ -112,13 +113,24 @@ export const CommentBlock = (props: CommentBlockProps) => { const isOwner = true // TODO: need to check against current user (not really feasible with modck data) const hasBadges = false // TODO: need to figure out how to data model these "badges" correctly + useEffect(() => { + if ( + isDeleting && + (deleteCommentStatus === Status.SUCCESS || + deleteCommentStatus === Status.ERROR) && + prevDeleteCommentStatus !== deleteCommentStatus + ) { + setIsDeleting(false) + } + }, [isDeleting, deleteCommentStatus, prevDeleteCommentStatus]) + const handleCommentEdit = (commentMessage: string) => { setShowEditInput(false) editComment(commentId, commentMessage) } const handleCommentReply = (commentMessage: string) => { - postComment(commentMessage, parentCommentId) + postComment(commentMessage, parentCommentId ?? comment.id) } const handleCommentReact = () => { @@ -127,7 +139,7 @@ export const CommentBlock = (props: CommentBlockProps) => { } const handleCommentDelete = () => { - // TODO: what should UI be doing here + setIsDeleting(true) deleteComment(commentId) } @@ -217,13 +229,17 @@ export const CommentBlock = (props: CommentBlockProps) => { ) : null} {/* TODO: rework this - this is a temporary design: just to have buttons for triggering stuff */} {isOwner ? ( - + isDeleting ? ( + + ) : ( + + ) ) : null} {isOwner ? ( { + const prevIsLoading = usePrevious(isLoading) + const { resetForm } = useFormikContext() + useEffect(() => { + if (!isLoading && prevIsLoading) { + resetForm() + } + }, [prevIsLoading, isLoading, resetForm]) + return null +} + export const CommentForm = ({ onSubmit, initialValue = '', @@ -36,18 +55,15 @@ export const CommentForm = ({ SquareSizes.SIZE_150_BY_150 ) - const handleSubmit = ( - { commentMessage }: CommentFormValues, - { resetForm }: FormikHelpers - ) => { + const handleSubmit = ({ commentMessage }: CommentFormValues) => { onSubmit?.(commentMessage) - resetForm() } const formInitialValues: CommentFormValues = { commentMessage: initialValue } return (
+ {!hideAvatar ? ( void, - CommentResponse + number > useReactToComment: WrappedMutationHook< (commentId: string, isLiked: boolean) => void, diff --git a/packages/web/src/components/comments/CommentSectionDesktop.tsx b/packages/web/src/components/comments/CommentSectionDesktop.tsx index d529f7ff6fb..b58f592615a 100644 --- a/packages/web/src/components/comments/CommentSectionDesktop.tsx +++ b/packages/web/src/components/comments/CommentSectionDesktop.tsx @@ -5,6 +5,7 @@ import { CommentForm } from './CommentForm' import { CommentHeader } from './CommentHeader' import { useCurrentCommentSection } from './CommentSectionContext' import { CommentThread } from './CommentThread' +import { NoComments } from './NoComments' export const CommentSectionDesktop = () => { const { @@ -63,6 +64,7 @@ export const CommentSectionDesktop = () => { ) : null} + {comments.length === 0 ? : null} {comments.map(({ id }) => ( ))} diff --git a/packages/web/src/components/comments/CommentThread.tsx b/packages/web/src/components/comments/CommentThread.tsx index e6ce0834158..cbd68cfe18f 100644 --- a/packages/web/src/components/comments/CommentThread.tsx +++ b/packages/web/src/components/comments/CommentThread.tsx @@ -2,6 +2,7 @@ import { useState } from 'react' import { useGetCommentById } from '@audius/common/api' import { Flex, IconCaretDown, IconCaretUp, TextLink } from '@audius/harmony' +import { ReplyComment } from '@audius/sdk/src/sdk/api/generated/default' import { CommentBlock } from './CommentBlock' import { useCurrentCommentSection } from './CommentSectionContext' @@ -44,7 +45,7 @@ export const CommentThread = ({ commentId }: { commentId: string }) => { ) : null} {hiddenReplies[rootComment.id] ? null : ( - {rootComment?.replies?.map((reply) => ( + {rootComment?.replies?.map((reply: ReplyComment) => (