From da63c2b7753fdbb6f5287ab1f82a2cf9f33aad12 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Tue, 27 Sep 2022 14:45:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=98=E5=8C=96=E5=B7=B2?= =?UTF-8?q?=E8=AF=BB=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BF=AE=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E4=BD=BF=E7=94=A8Intersection=E6=9D=A5=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E6=98=AF=E5=90=A6=E5=B7=B2=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/shared/event/index.ts | 5 ++ client/shared/redux/hooks/useConverseAck.ts | 58 +++++++-------- client/shared/redux/slices/chat.ts | 2 +- .../ChatBox/ChatMessageList/Item.tsx | 9 ++- .../ChatBox/ChatMessageList/NormalList.tsx | 10 +-- .../ChatMessageList/VirtualizedList.tsx | 7 -- .../ChatBox/ChatMessageList/types.ts | 1 - client/web/src/components/ChatBox/index.tsx | 3 +- .../src/components/ChatBox/useMessageAck.ts | 34 ++++----- client/web/src/components/Intersection.tsx | 70 +++++++++++++++++++ 10 files changed, 128 insertions(+), 71 deletions(-) create mode 100644 client/web/src/components/Intersection.tsx diff --git a/client/shared/event/index.ts b/client/shared/event/index.ts index a1b0387a..4e4c0d4c 100644 --- a/client/shared/event/index.ts +++ b/client/shared/event/index.ts @@ -30,6 +30,11 @@ export interface SharedEventMap { * 如果为null则是清空 */ replyMessage: (payload: ChatMessage | null) => void; + + /** + * 消息已读(消息出现在界面上) + */ + readMessage: (payload: ChatMessage | null) => void; } export type SharedEventType = keyof SharedEventMap; diff --git a/client/shared/redux/hooks/useConverseAck.ts b/client/shared/redux/hooks/useConverseAck.ts index 9cf4b7b0..b1ea718b 100644 --- a/client/shared/redux/hooks/useConverseAck.ts +++ b/client/shared/redux/hooks/useConverseAck.ts @@ -1,9 +1,18 @@ -import { useCallback, useMemo, useRef } from 'react'; +import { useRef } from 'react'; import { useAppDispatch, useAppSelector } from './useAppSelector'; import _debounce from 'lodash/debounce'; import { isValidStr } from '../../utils/string-helper'; import { chatActions } from '../slices'; import { updateAck } from '../../model/converse'; +import { useMemoizedFn } from '../../hooks/useMemoizedFn'; + +const updateAckDebounce = _debounce( + (converseId: string, lastMessageId: string) => { + updateAck(converseId, lastMessageId); + }, + 1000, + { leading: true, trailing: true } +); /** * 会话已读信息管理 @@ -19,41 +28,32 @@ export function useConverseAck(converseId: string) { (state) => state.chat.ack[converseId] ?? '' ); - const setConverseAck = useMemo( - () => - _debounce( - (converseId: string, lastMessageId: string) => { - if ( - isValidStr(lastMessageIdRef.current) && - lastMessageId <= lastMessageIdRef.current - ) { - // 更新的数字比较小,跳过 - return; - } - - dispatch(chatActions.setConverseAck({ converseId, lastMessageId })); - updateAck(converseId, lastMessageId); - lastMessageIdRef.current = lastMessageId; - }, - 1000, - { leading: true, trailing: true } - ), - [] + const setConverseAck = useMemoizedFn( + (converseId: string, lastMessageId: string) => { + if ( + isValidStr(lastMessageIdRef.current) && + lastMessageId <= lastMessageIdRef.current + ) { + // 更新的数字比较小,跳过 + return; + } + + dispatch(chatActions.setConverseAck({ converseId, lastMessageId })); + updateAckDebounce(converseId, lastMessageId); + lastMessageIdRef.current = lastMessageId; + } ); /** * 更新会话最新消息 */ - const updateConverseAck = useCallback( - (lastMessageId: string) => { - setConverseAck(converseId, lastMessageId); - }, - [converseId] - ); + const updateConverseAck = useMemoizedFn((lastMessageId: string) => { + setConverseAck(converseId, lastMessageId); + }); - const markConverseAllAck = useCallback(() => { + const markConverseAllAck = useMemoizedFn(() => { updateConverseAck(converseLastMessage); - }, [converseLastMessage]); + }); return { updateConverseAck, markConverseAllAck }; } diff --git a/client/shared/redux/slices/chat.ts b/client/shared/redux/slices/chat.ts index 1229e9cb..56ec6b1f 100644 --- a/client/shared/redux/slices/chat.ts +++ b/client/shared/redux/slices/chat.ts @@ -86,7 +86,7 @@ const chatSlice = createSlice({ state.converses[converseId].messages = newMessages; if (state.currentConverseId !== converseId) { - const lastMessageId = _last(messages)?._id; + const lastMessageId = _last(newMessages)?._id; if (isValidStr(lastMessageId)) { state.lastMessageMap[converseId] = lastMessageId; } diff --git a/client/web/src/components/ChatBox/ChatMessageList/Item.tsx b/client/web/src/components/ChatBox/ChatMessageList/Item.tsx index 86379a67..67a1f0cf 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/Item.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/Item.tsx @@ -8,6 +8,7 @@ import { t, useCachedUserInfo, MessageHelper, + sharedEvent, } from 'tailchat-shared'; import { useRenderPluginMessageInterpreter } from './useRenderPluginMessageInterpreter'; import { getMessageRender, pluginMessageExtraParsers } from '@/plugin/common'; @@ -21,6 +22,7 @@ import { useMessageReactions } from './useMessageReactions'; import { stopPropagation } from '@/utils/dom-helper'; import { useUserInfoList } from 'tailchat-shared/hooks/model/useUserInfoList'; import { AutoFolder, Avatar, Icon } from 'tailchat-design'; +import { Intersection } from '@/components/Intersection'; import './Item.less'; /** @@ -279,7 +281,12 @@ export function buildMessageItemRow(messages: ChatMessage[], index: number) { {getMessageTimeDiff(messageCreatedAt)} )} - + + sharedEvent.emit('readMessage', message)} + > + + ); } diff --git a/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx b/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx index c3428164..2b4236d8 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/NormalList.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useRef } from 'react'; -import { sharedEvent, useUpdateRef } from 'tailchat-shared'; +import { sharedEvent } from 'tailchat-shared'; import { buildMessageItemRow } from './Item'; import type { MessageListProps } from './types'; @@ -10,18 +10,10 @@ export const NormalMessageList: React.FC = React.memo( (props) => { const containerRef = useRef(null); - const onUpdateReadedMessageRef = useUpdateRef(props.onUpdateReadedMessage); useEffect(() => { if (props.messages.length === 0) { return; } - - if (containerRef.current?.scrollTop === 0) { - // 当前列表在最低 - onUpdateReadedMessageRef.current( - props.messages[props.messages.length - 1]._id - ); - } }, [props.messages.length]); useEffect(() => { diff --git a/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx b/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx index 8d9388a1..612445d7 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx +++ b/client/web/src/components/ChatBox/ChatMessageList/VirtualizedList.tsx @@ -7,7 +7,6 @@ import { VirtuosoHandle, } from 'react-virtuoso'; import { ChatMessage, sharedEvent, useMemoizedFn } from 'tailchat-shared'; -import _last from 'lodash/last'; const PREPEND_OFFSET = 10 ** 7; @@ -53,12 +52,6 @@ export const VirtualizedMessageList: React.FC = React.memo( const followOutput = useMemoizedFn( (isAtBottom: boolean): FollowOutputScalarType => { if (isAtBottom) { - // 更新最新查看的消息id - const lastMessage = _last(props.messages); - if (lastMessage) { - props.onUpdateReadedMessage(lastMessage._id); - } - setTimeout(() => { // 这里 Virtuoso 有个动态渲染高度的bug, 因此需要异步再次滚动到底部以确保代码功能work listRef.current?.scrollToIndex({ diff --git a/client/web/src/components/ChatBox/ChatMessageList/types.ts b/client/web/src/components/ChatBox/ChatMessageList/types.ts index 5256ee14..de6a0303 100644 --- a/client/web/src/components/ChatBox/ChatMessageList/types.ts +++ b/client/web/src/components/ChatBox/ChatMessageList/types.ts @@ -4,6 +4,5 @@ export interface MessageListProps { messages: ChatMessage[]; isLoadingMore: boolean; hasMoreMessage: boolean; - onUpdateReadedMessage: (lastMessageId: string) => void; onLoadMore: () => Promise; } diff --git a/client/web/src/components/ChatBox/index.tsx b/client/web/src/components/ChatBox/index.tsx index 64629497..3e3e4064 100644 --- a/client/web/src/components/ChatBox/index.tsx +++ b/client/web/src/components/ChatBox/index.tsx @@ -33,7 +33,7 @@ const ChatBoxInner: React.FC = React.memo((props) => { converseId, isGroup, }); - const { updateConverseAck } = useMessageAck(converseId, messages); + useMessageAck(converseId); if (loading) { return ; @@ -50,7 +50,6 @@ const ChatBoxInner: React.FC = React.memo((props) => { messages={messages} isLoadingMore={isLoadingMore} hasMoreMessage={hasMoreMessage} - onUpdateReadedMessage={updateConverseAck} onLoadMore={handleFetchMoreMessage} /> diff --git a/client/web/src/components/ChatBox/useMessageAck.ts b/client/web/src/components/ChatBox/useMessageAck.ts index d136f305..c3fd0b2b 100644 --- a/client/web/src/components/ChatBox/useMessageAck.ts +++ b/client/web/src/components/ChatBox/useMessageAck.ts @@ -1,32 +1,24 @@ import { useEffect } from 'react'; -import { - ChatMessage, - useConverseAck, - useMemoizedFn, - useUpdateRef, -} from 'tailchat-shared'; -import _last from 'lodash/last'; +import { ChatMessage, sharedEvent, useConverseAck } from 'tailchat-shared'; /** * 消息已读的回调 */ -export function useMessageAck(converseId: string, messages: ChatMessage[]) { +export function useMessageAck(converseId: string) { const { updateConverseAck } = useConverseAck(converseId); - const messagesRef = useUpdateRef(messages); - const updateConverseAckMemo = useMemoizedFn(updateConverseAck); useEffect(() => { - // 设置当前 - if (messagesRef.current.length === 0) { - return; - } + const handldReadMessage = (message: ChatMessage | null) => { + const messageId = message?._id; + if (messageId && converseId === message.converseId) { + updateConverseAck(messageId); + } + }; - const lastMessage = _last(messagesRef.current); - if (lastMessage) { - const lastMessageId = lastMessage?._id; - updateConverseAckMemo(lastMessageId); - } - }, [converseId]); + sharedEvent.on('readMessage', handldReadMessage); - return { updateConverseAck }; + return () => { + sharedEvent.off('readMessage', handldReadMessage); + }; + }, [converseId]); } diff --git a/client/web/src/components/Intersection.tsx b/client/web/src/components/Intersection.tsx new file mode 100644 index 00000000..f45f5048 --- /dev/null +++ b/client/web/src/components/Intersection.tsx @@ -0,0 +1,70 @@ +/** + * 监测是否可见 + */ + +import React, { PropsWithChildren, useEffect, useRef } from 'react'; +import { useMemoizedFn } from 'tailchat-shared'; + +interface IntersectionProps extends PropsWithChildren { + root?: string; + rootMargin?: string; + + /** + * Either a single number or an array of numbers which indicate at what percentage of the target's visibility the observer's callback should be executed. If you only want to detect when visibility passes the 50% mark, you can use a value of 0.5. If you want the callback to run every time visibility passes another 25%, you would specify the array [0, 0.25, 0.5, 0.75, 1]. The default is 0 (meaning as soon as even one pixel is visible, the callback will be run). A value of 1.0 means that the threshold isn't considered passed until every pixel is visible. + */ + threshold?: number | number[]; + onIntersection?: (target: Element) => void; + onUnIntersection?: (target: Element) => void; + onIntersectionUnmount?: () => void; +} +export const Intersection: React.FC = React.memo((props) => { + const ref = useRef(null); + + const handleIntersectionChange = useMemoizedFn( + (entries: IntersectionObserverEntry[]) => { + const { onIntersection, onUnIntersection } = props; + entries.forEach((entry) => { + /** + * Reference: https://bugs.chromium.org/p/chromium/issues/detail?id=713819 + */ + const { intersectionRatio } = entry; + if (intersectionRatio > 0) { + if (onIntersection) { + onIntersection(entry.target); + } + } else if (onUnIntersection) { + onUnIntersection(entry.target); + } + }); + } + ); + + const handleIntersectionUnmount = useMemoizedFn(() => { + props.onIntersectionUnmount && props.onIntersectionUnmount(); + }); + + useEffect(() => { + if (!ref.current) { + return; + } + + const root = props.root ? document.querySelector(props.root) : null; + const intersectionOberver = new IntersectionObserver( + handleIntersectionChange, + { + root, + rootMargin: props.rootMargin, + threshold: props.threshold, + } + ); + intersectionOberver.observe(ref.current); + + return () => { + intersectionOberver.disconnect(); + handleIntersectionUnmount(); + }; + }, []); + + return
{props.children}
; +}); +Intersection.displayName = 'Intersection';