From 3aaeaa8085152cb46f1dd25721cdbbb390fb0d53 Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Mon, 7 Nov 2022 01:06:04 +0800 Subject: [PATCH] [question][feat] update question answers,comments optimistically --- .../[questionId]/[questionSlug]/index.tsx | 58 ++++---- .../questions/questions-answer-router.ts | 5 +- .../questions-question-comment-router.ts | 4 +- .../questions-question-comment-user-router.ts | 13 +- apps/portal/src/utils/questions/useVote.ts | 138 +++++++++++++++--- 5 files changed, 157 insertions(+), 61 deletions(-) diff --git a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx index 5413a663..9fdee125 100644 --- a/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx +++ b/apps/portal/src/pages/questions/[questionId]/[questionSlug]/index.tsx @@ -245,19 +245,18 @@ export default function QuestionPage() { /> - {(commentData?.pages ?? []).flatMap( - ({ processedQuestionCommentsData: comments }) => - comments.map((comment) => ( - - )), + {(commentData?.pages ?? []).flatMap(({ data: comments }) => + comments.map((comment) => ( + + )), )}
{/* TODO: Add button to load more */} - {(answerData?.pages ?? []).flatMap( - ({ processedAnswersData: answers }) => - answers.map((answer) => ( - - )), + {(answerData?.pages ?? []).flatMap(({ data: answers }) => + answers.map((answer) => ( + + )), )} diff --git a/apps/portal/src/server/router/questions/questions-answer-router.ts b/apps/portal/src/server/router/questions/questions-answer-router.ts index e2e11715..a1c51b56 100644 --- a/apps/portal/src/server/router/questions/questions-answer-router.ts +++ b/apps/portal/src/server/router/questions/questions-answer-router.ts @@ -38,7 +38,6 @@ export const questionsAnswerRouter = createRouter() }, ]; - const answersData = await ctx.prisma.questionsAnswer.findMany({ cursor: cursor ? { id: cursor } : undefined, include: { @@ -104,9 +103,9 @@ export const questionsAnswerRouter = createRouter() } return { + data: processedAnswersData, nextCursor, - processedAnswersData, - } + }; }, }) .query('getAnswerById', { diff --git a/apps/portal/src/server/router/questions/questions-question-comment-router.ts b/apps/portal/src/server/router/questions/questions-question-comment-router.ts index 9f42b88e..8ac0ff0b 100644 --- a/apps/portal/src/server/router/questions/questions-question-comment-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-comment-router.ts @@ -97,9 +97,9 @@ export const questionsQuestionCommentRouter = createRouter().query( } return { + data: processedQuestionCommentsData, nextCursor, - processedQuestionCommentsData, - } + }; }, }, ); diff --git a/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts b/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts index f2b9afb9..943423a9 100644 --- a/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-comment-user-router.ts @@ -256,18 +256,15 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter() } if (vote.vote === Vote.UPVOTE) { - tx.questionsQuestionCommentVote.delete({ - where: { - id: vote.id, - }, - }); - - const createdVote = await tx.questionsQuestionCommentVote.create({ + const updatedVote = await tx.questionsQuestionCommentVote.update({ data: { questionCommentId, userId, vote: Vote.DOWNVOTE, }, + where: { + id: vote.id, + }, }); await tx.questionsQuestionComment.update({ @@ -281,7 +278,7 @@ export const questionsQuestionCommentUserRouter = createProtectedRouter() }, }); - return createdVote; + return updatedVote; } }); }, diff --git a/apps/portal/src/utils/questions/useVote.ts b/apps/portal/src/utils/questions/useVote.ts index 306080b8..b64bd79c 100644 --- a/apps/portal/src/utils/questions/useVote.ts +++ b/apps/portal/src/utils/questions/useVote.ts @@ -5,7 +5,7 @@ import { Vote } from '@prisma/client'; import { trpc } from '../trpc'; -import type { Question } from '~/types/questions'; +import type { Answer, Question, QuestionComment } from '~/types/questions'; type UseVoteOptions = { setDownVote: () => void; @@ -67,13 +67,11 @@ export const useQuestionVote = (id: string) => { // 'questions.questions.getQuestionById', // 'questions.questions.getQuestionsByFilterAndContent', ], - onMutate: async (previousVote, currentVote) => { + onMutate: async (voteValueChange) => { // Update question list const questionQueries = utils.queryClient.getQueriesData([ 'questions.questions.getQuestionsByFilterAndContent', ]); - const voteValueChange = - getVoteValue(currentVote) - getVoteValue(previousVote); if (questionQueries !== undefined) { for (const [key, query] of questionQueries) { @@ -133,12 +131,70 @@ export const useQuestionVote = (id: string) => { }; export const useAnswerVote = (id: string) => { + const utils = trpc.useContext(); + return useVote(id, { idKey: 'answerId', invalidateKeys: [ - 'questions.answers.getAnswerById', - 'questions.answers.getAnswers', + // 'questions.answers.getAnswerById', + // 'questions.answers.getAnswers', ], + onMutate: async (voteValueChange) => { + // Update question answer list + const answerQueries = utils.queryClient.getQueriesData([ + 'questions.answers.getAnswers', + ]); + + if (answerQueries !== undefined) { + for (const [key, query] of answerQueries) { + if (query === undefined) { + continue; + } + + const { pages, ...restQuery } = query as InfiniteData<{ + data: Array; + }>; + + const newQuery = { + pages: pages.map(({ data, ...restPage }) => ({ + data: data.map((answer) => { + if (answer.id === id) { + const { numVotes, ...restAnswer } = answer; + return { + numVotes: numVotes + voteValueChange, + ...restAnswer, + }; + } + return answer; + }), + ...restPage, + })), + ...restQuery, + }; + + utils.queryClient.setQueryData(key, newQuery); + } + } + + const prevAnswer = utils.queryClient.getQueryData([ + 'questions.answers.getAnswerById', + { + answerId: id, + }, + ]) as Answer | undefined; + + if (prevAnswer !== undefined) { + const newAnswer = { + ...prevAnswer, + numVotes: prevAnswer.numVotes + voteValueChange, + }; + + utils.queryClient.setQueryData( + ['questions.answers.getAnswerById', { answerId: id }], + newAnswer, + ); + } + }, query: 'questions.answers.user.getVote', setDownVoteKey: 'questions.answers.user.setDownVote', setNoVoteKey: 'questions.answers.user.setNoVote', @@ -147,9 +203,48 @@ export const useAnswerVote = (id: string) => { }; export const useQuestionCommentVote = (id: string) => { + const utils = trpc.useContext(); + return useVote(id, { idKey: 'questionCommentId', - invalidateKeys: ['questions.questions.comments.getQuestionComments'], + invalidateKeys: [], + onMutate: async (voteValueChange) => { + // Update question comment list + const questionCommentQueries = utils.queryClient.getQueriesData([ + 'questions.questions.comments.getQuestionComments', + ]); + + if (questionCommentQueries !== undefined) { + for (const [key, query] of questionCommentQueries) { + if (query === undefined) { + continue; + } + + const { pages, ...restQuery } = query as InfiniteData<{ + data: Array; + }>; + + const newQuery = { + pages: pages.map(({ data, ...restPage }) => ({ + data: data.map((questionComment) => { + if (questionComment.id === id) { + const { numVotes, ...restQuestionComment } = questionComment; + return { + numVotes: numVotes + voteValueChange, + ...restQuestionComment, + }; + } + return questionComment; + }), + ...restPage, + })), + ...restQuery, + }; + + utils.queryClient.setQueryData(key, newQuery); + } + } + }, query: 'questions.questions.comments.user.getVote', setDownVoteKey: 'questions.questions.comments.user.setDownVote', setNoVoteKey: 'questions.questions.comments.user.setNoVote', @@ -168,10 +263,7 @@ export const useAnswerCommentVote = (id: string) => { }); }; -type InvalidateFunction = ( - previousVote: Vote | null, - currentVote: Vote | null, -) => Promise; +type InvalidateFunction = (voteValueChange: number) => Promise; type VoteProps = { idKey: string; @@ -205,7 +297,7 @@ export const useVote = ( } = opts; const utils = trpc.useContext(); - const onVoteUpdate = useCallback(() => { + const onVoteUpdateSettled = useCallback(() => { // TODO: Optimise query invalidation // utils.invalidateQueries([query, { [idKey]: id } as any]); for (const invalidateKey of invalidateKeys) { @@ -256,10 +348,14 @@ export const useVote = ( currentData as any, ); - await onMutate?.(previousData?.vote ?? null, currentData?.vote ?? null); + const voteValueChange = + getVoteValue(currentData?.vote ?? null) - + getVoteValue(previousData?.vote ?? null); + + await onMutate?.(voteValueChange); return { currentData, previousData }; }, - onSettled: onVoteUpdate, + onSettled: onVoteUpdateSettled, }, ); const { mutate: setDownVote } = trpc.useMutation( @@ -291,10 +387,14 @@ export const useVote = ( currentData as any, ); - await onMutate?.(previousData?.vote ?? null, currentData?.vote ?? null); + const voteValueChange = + getVoteValue(currentData?.vote ?? null) - + getVoteValue(previousData?.vote ?? null); + + await onMutate?.(voteValueChange); return { currentData, previousData }; }, - onSettled: onVoteUpdate, + onSettled: onVoteUpdateSettled, }, ); @@ -323,11 +423,13 @@ export const useVote = ( currentData, ); - await onMutate?.(previousData?.vote ?? null, null); + const voteValueChange = + getVoteValue(null) - getVoteValue(previousData?.vote ?? null); + await onMutate?.(voteValueChange); return { currentData, previousData }; }, - onSettled: onVoteUpdate, + onSettled: onVoteUpdateSettled, }, );