From d8595b85384b9e1c67cc629258bb8c7e2046276e Mon Sep 17 00:00:00 2001 From: Innei Date: Sat, 20 Apr 2024 23:25:38 +0800 Subject: [PATCH] feat: new comment observer Signed-off-by: Innei --- .../modules/comment/CommentBox/hooks.tsx | 4 +- .../modules/comment/CommentPinButton.tsx | 4 +- src/components/modules/comment/Comments.tsx | 38 +++++++++++++++++-- src/lib/biz.ts | 19 ++++++++++ src/queries/keys/comment.ts | 1 + src/queries/keys/index.ts | 1 + src/socket/handler.ts | 35 ++++++++++++++++- src/socket/util.ts | 32 ++++++++++++++++ 8 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 src/lib/biz.ts create mode 100644 src/queries/keys/comment.ts create mode 100644 src/queries/keys/index.ts create mode 100644 src/socket/util.ts diff --git a/src/components/modules/comment/CommentBox/hooks.tsx b/src/components/modules/comment/CommentBox/hooks.tsx index b6d32a02a2..8fbefd7db0 100644 --- a/src/components/modules/comment/CommentBox/hooks.tsx +++ b/src/components/modules/comment/CommentBox/hooks.tsx @@ -21,8 +21,8 @@ import { apiClient } from '~/lib/request' import { getErrorMessageFromRequestError } from '~/lib/request.shared' import { jotaiStore } from '~/lib/store' import { toast } from '~/lib/toast' +import { buildCommentsQueryKey } from '~/queries/keys' -import { buildQueryKey } from '../Comments' import { MAX_COMMENT_TEXT_LENGTH } from './constants' import { CommentBoxContext, @@ -221,7 +221,7 @@ export const useSendComment = () => { ? '感谢你的回复!' : '感谢你的评论!' - const commentListQueryKey = buildQueryKey(originalRefId) + const commentListQueryKey = buildCommentsQueryKey(originalRefId) toast.success(toastCopy) jotaiStore.set(textAtom, '') diff --git a/src/components/modules/comment/CommentPinButton.tsx b/src/components/modules/comment/CommentPinButton.tsx index b152c537d1..db80fa596d 100644 --- a/src/components/modules/comment/CommentPinButton.tsx +++ b/src/components/modules/comment/CommentPinButton.tsx @@ -6,10 +6,10 @@ import type { Draft } from 'immer' import type { SVGProps } from 'react' import { apiClient } from '~/lib/request' +import { buildCommentsQueryKey } from '~/queries/keys' import { PinIconToggle } from '../shared/PinIconToggle' import { useCommentBoxRefIdValue } from './CommentBox/hooks' -import { buildQueryKey } from './Comments' export const CommentPinButton = ({ comment }: { comment: CommentModel }) => { const queryClient = useQueryClient() @@ -20,7 +20,7 @@ export const CommentPinButton = ({ comment }: { comment: CommentModel }) => { pin={!!comment.pin} onPinChange={async (nextPin) => { queryClient.setQueryData>>( - buildQueryKey(refId), + buildCommentsQueryKey(refId), (old) => { return produce(old, (draft) => { if (!draft) return draft diff --git a/src/components/modules/comment/Comments.tsx b/src/components/modules/comment/Comments.tsx index 9d8fd805e9..c07d122c03 100644 --- a/src/components/modules/comment/Comments.tsx +++ b/src/components/modules/comment/Comments.tsx @@ -1,23 +1,55 @@ 'use client' import { useInfiniteQuery } from '@tanstack/react-query' -import { memo, useMemo } from 'react' +import { memo, useEffect, useMemo } from 'react' import type { FC } from 'react' import type { CommentBaseProps } from './types' +import { BusinessEvents } from '@mx-space/webhook' + import { ErrorBoundary } from '~/components/common/ErrorBoundary' import { NotSupport } from '~/components/common/NotSupport' import { BottomToUpSoftScaleTransitionView } from '~/components/ui/transition' import { apiClient } from '~/lib/request' +import { buildCommentsQueryKey } from '~/queries/keys' +import { WsEvent } from '~/socket/util' import { LoadMoreIndicator } from '../shared/LoadMoreIndicator' import { Comment } from './Comment' import { CommentBoxProvider } from './CommentBox/providers' import { CommentSkeleton } from './CommentSkeleton' -export const buildQueryKey = (refId: string) => ['comments', refId] +const useNewCommentObserver = (refId: string) => { + useEffect(() => { + const currentTitle = document.title + + // 当标签页回复前台状态时,将标题重置 + const onVisibilityChange = () => { + if (document.visibilityState === 'visible') { + document.title = currentTitle + } + } + document.addEventListener('visibilitychange', onVisibilityChange) + + const cleaner = WsEvent.on(BusinessEvents.COMMENT_CREATE, (data: any) => { + if (data.ref === refId) { + // 如果标签页在后台 + + if (document.visibilityState === 'hidden') { + document.title = `新评论!${currentTitle}` + } + } + }) + return () => { + cleaner() + document.removeEventListener('visibilitychange', onVisibilityChange) + } + }, [refId]) +} export const Comments: FC = ({ refId }) => { - const key = useMemo(() => buildQueryKey(refId), [refId]) + useNewCommentObserver(refId) + + const key = useMemo(() => buildCommentsQueryKey(refId), [refId]) const { data, isLoading, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey: key, queryFn: async ({ queryKey, pageParam }) => { diff --git a/src/lib/biz.ts b/src/lib/biz.ts new file mode 100644 index 0000000000..a3eb2488e0 --- /dev/null +++ b/src/lib/biz.ts @@ -0,0 +1,19 @@ +import { getCurrentNoteData } from '~/providers/note/CurrentNoteDataProvider' +import { getGlobalCurrentPostData } from '~/providers/post/CurrentPostDataProvider' + +import { isServerSide } from './env' + +export const getCurrentPageId = () => { + if (isServerSide) return + const pathname = window.location.pathname + + if (pathname.startsWith('/notes/')) { + const noteId = getCurrentNoteData() + + return noteId?.data.id + } + + if (pathname.startsWith('/posts/')) { + return getGlobalCurrentPostData().id + } +} diff --git a/src/queries/keys/comment.ts b/src/queries/keys/comment.ts new file mode 100644 index 0000000000..007de4332c --- /dev/null +++ b/src/queries/keys/comment.ts @@ -0,0 +1 @@ +export const buildCommentsQueryKey = (refId: string) => ['comments', refId] diff --git a/src/queries/keys/index.ts b/src/queries/keys/index.ts new file mode 100644 index 0000000000..ed051c229a --- /dev/null +++ b/src/queries/keys/index.ts @@ -0,0 +1 @@ +export * from './comment' diff --git a/src/socket/handler.ts b/src/socket/handler.ts index 531a987b6d..9c70a493bb 100644 --- a/src/socket/handler.ts +++ b/src/socket/handler.ts @@ -2,12 +2,14 @@ import { queryClient } from '~/providers/root/react-query-provider' import React from 'react' import { produce } from 'immer' import type { + CommentModel, NoteModel, PaginateResult, PostModel, RecentlyModel, SayModel, } from '@mx-space/api-client' +import type { BusinessEvents } from '@mx-space/webhook' import type { InfiniteData } from '@tanstack/react-query' import type { ActivityPresence } from '~/models/activity' import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime' @@ -43,8 +45,11 @@ import { setGlobalCurrentPostData, } from '~/providers/post/CurrentPostDataProvider' import { queries } from '~/queries/definition' +import { buildCommentsQueryKey } from '~/queries/keys' import { EventTypes } from '~/types/events' +import { WsEvent } from './util' + const trackerRealtimeEvent = () => { document.dispatchEvent( new CustomEvent('impression', { @@ -250,6 +255,34 @@ export const eventHandler = ( break } + case EventTypes.COMMENT_CREATE: { + const payload = data as { + ref: string + id: string + } + + const queryData = queryClient.getQueryData< + InfiniteData> + >(buildCommentsQueryKey(payload.ref)) + + if (!queryData) return + for (const page of queryData.pages) { + if (page.data.some((comment) => comment.id === payload.id)) { + return + } + } + + requestAnimationFrame(() => { + requestAnimationFrame(() => { + queryClient.invalidateQueries({ + queryKey: buildCommentsQueryKey(payload.ref), + }) + }) + }) + + break + } + case EventTypes.ACTIVITY_LEAVE_PRESENCE: { const payload = data as { identity: string @@ -313,13 +346,13 @@ export const eventHandler = ( } default: { - window.dispatchEvent(new CustomEvent(`event:${type}`, { detail: data })) if (isDev) { // eslint-disable-next-line no-console console.log(type, data) } } } + WsEvent.emit(type as BusinessEvents, data) } interface ProcessInfo { diff --git a/src/socket/util.ts b/src/socket/util.ts new file mode 100644 index 0000000000..911f95c391 --- /dev/null +++ b/src/socket/util.ts @@ -0,0 +1,32 @@ +import type { BusinessEvents } from '@mx-space/webhook' + +export const buildSocketEventType = (type: string) => + `ws_event:${type}` as const + +export class WsEvent extends Event { + constructor( + type: BusinessEvents, + public data: unknown, + ) { + super(buildSocketEventType(type)) + } + + static on( + type: BusinessEvents, + + cb: (data: unknown) => void, + ) { + const _cb = (e: any) => { + cb(e.data) + } + document.addEventListener(buildSocketEventType(type), _cb) + + return () => { + document.removeEventListener(buildSocketEventType(type), _cb) + } + } + + static emit(type: BusinessEvents, data: unknown) { + document.dispatchEvent(new WsEvent(type, data)) + } +}