From f08b1223226b8a4876a1d637b36b14d6f6896f61 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Thu, 9 Sep 2021 20:45:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E4=BC=9A=E8=AF=9Dack=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/hooks/useDebounce.ts | 17 ++++++ shared/hooks/useTimeoutFn.ts | 44 +++++++++++++ shared/index.tsx | 3 +- shared/model/converse.ts | 9 +++ shared/redux/hooks/useConverseMessage.ts | 4 +- shared/redux/slices/chat.ts | 16 +++++ .../ChatBox/ChatMessageList/index.tsx | 28 ++++++++- web/src/components/ChatBox/index.tsx | 9 ++- web/src/components/ChatBox/useMessageAck.ts | 61 +++++++++++++++++++ 9 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 shared/hooks/useDebounce.ts create mode 100644 shared/hooks/useTimeoutFn.ts create mode 100644 web/src/components/ChatBox/useMessageAck.ts diff --git a/shared/hooks/useDebounce.ts b/shared/hooks/useDebounce.ts new file mode 100644 index 00000000..56f2d692 --- /dev/null +++ b/shared/hooks/useDebounce.ts @@ -0,0 +1,17 @@ +import { DependencyList, useEffect } from 'react'; +import { useTimeoutFn } from './useTimeoutFn'; + +export type UseDebounceReturn = [() => boolean | null, () => void]; + +export function useDebounce( + // eslint-disable-next-line @typescript-eslint/ban-types + fn: Function, + ms = 0, + deps: DependencyList = [] +): UseDebounceReturn { + const [isReady, cancel, reset] = useTimeoutFn(fn, ms); + + useEffect(reset, deps); + + return [isReady, cancel]; +} diff --git a/shared/hooks/useTimeoutFn.ts b/shared/hooks/useTimeoutFn.ts new file mode 100644 index 00000000..858d287a --- /dev/null +++ b/shared/hooks/useTimeoutFn.ts @@ -0,0 +1,44 @@ +import { useCallback, useEffect, useRef } from 'react'; + +export type UseTimeoutFnReturn = [() => boolean | null, () => void, () => void]; + +export function useTimeoutFn( + // eslint-disable-next-line @typescript-eslint/ban-types + fn: Function, + ms = 0 +): UseTimeoutFnReturn { + const ready = useRef(false); + const timeout = useRef>(); + const callback = useRef(fn); + + const isReady = useCallback(() => ready.current, []); + + const set = useCallback(() => { + ready.current = false; + timeout.current && clearTimeout(timeout.current); + + timeout.current = setTimeout(() => { + ready.current = true; + callback.current(); + }, ms); + }, [ms]); + + const clear = useCallback(() => { + ready.current = null; + timeout.current && clearTimeout(timeout.current); + }, []); + + // update ref when function changes + useEffect(() => { + callback.current = fn; + }, [fn]); + + // set on mount, clear on unmount + useEffect(() => { + set(); + + return clear; + }, [ms]); + + return [isReady, clear, set]; +} diff --git a/shared/index.tsx b/shared/index.tsx index 8814282a..c978bb76 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -41,6 +41,7 @@ export { Trans } from './i18n/Trans'; export { useAsync } from './hooks/useAsync'; export { useAsyncFn } from './hooks/useAsyncFn'; export { useAsyncRequest } from './hooks/useAsyncRequest'; +export { useDebounce } from './hooks/useDebounce'; export { useMountedState } from './hooks/useMountedState'; export { useRafState } from './hooks/useRafState'; export { useUpdateRef } from './hooks/useUpdateRef'; @@ -65,7 +66,7 @@ export { // model export { fetchAvailableServices } from './model/common'; -export { createDMConverse } from './model/converse'; +export { createDMConverse, updateAck } from './model/converse'; export { addFriendRequest, cancelFriendRequest, diff --git a/shared/model/converse.ts b/shared/model/converse.ts index f6784211..b7df3f98 100644 --- a/shared/model/converse.ts +++ b/shared/model/converse.ts @@ -36,3 +36,12 @@ export async function fetchConverseInfo( return data; } + +/** + * 更新会话已读 + * @param converseId 会话ID + * @param lastMessageId 最后一条消息ID + */ +export async function updateAck(converseId: string, lastMessageId: string) { + await request.post('/api/chat/ack/update', { converseId, lastMessageId }); +} diff --git a/shared/redux/hooks/useConverseMessage.ts b/shared/redux/hooks/useConverseMessage.ts index a9f79253..18d0b245 100644 --- a/shared/redux/hooks/useConverseMessage.ts +++ b/shared/redux/hooks/useConverseMessage.ts @@ -47,11 +47,11 @@ export function useConverseMessage(context: ConverseContext) { } // Step 2. 拉取消息 - const messages = await fetchConverseMessage(converseId); + const historyMessages = await fetchConverseMessage(converseId); dispatch( chatActions.initialHistoryMessage({ converseId, - historyMessages: messages, + historyMessages, }) ); } else { diff --git a/shared/redux/slices/chat.ts b/shared/redux/slices/chat.ts index 568210f2..7e30efa0 100644 --- a/shared/redux/slices/chat.ts +++ b/shared/redux/slices/chat.ts @@ -11,10 +11,12 @@ export interface ChatConverseState extends ChatConverseInfo { interface ChatState { converses: Record; + ack: Record; } const initialState: ChatState = { converses: {}, + ack: {}, }; const chatSlice = createSlice({ @@ -85,6 +87,20 @@ const chatSlice = createSlice({ state.converses[converseId].hasFetchedHistory = true; }, + + /** + * 设置已读消息 + */ + setConverseAck( + state, + action: PayloadAction<{ + converseId: string; + lastMessageId: string; + }> + ) { + const { converseId, lastMessageId } = action.payload; + state.ack[converseId] = lastMessageId; + }, }, }); diff --git a/web/src/components/ChatBox/ChatMessageList/index.tsx b/web/src/components/ChatBox/ChatMessageList/index.tsx index cd6b936d..70c5850b 100644 --- a/web/src/components/ChatBox/ChatMessageList/index.tsx +++ b/web/src/components/ChatBox/ChatMessageList/index.tsx @@ -1,14 +1,21 @@ -import React, { useImperativeHandle, useRef } from 'react'; +import React, { + useCallback, + useEffect, + useImperativeHandle, + useRef, +} from 'react'; import { ChatMessage, getMessageTimeDiff, shouldShowMessageTime, + useUpdateRef, } from 'tailchat-shared'; import { ChatMessageItem } from './Item'; import { Divider } from 'antd'; interface ChatMessageListProps { messages: ChatMessage[]; + onUpdateReadedMessage: (lastMessageId: string) => void; } export interface ChatMessageListRef { scrollToBottom: () => void; @@ -34,10 +41,29 @@ export const ChatMessageList = React.forwardRef< }, })); + const onUpdateReadedMessageRef = useUpdateRef(props.onUpdateReadedMessage); + useEffect(() => { + if (containerRef.current?.scrollTop === 0) { + // 当前列表在最低 + onUpdateReadedMessageRef.current( + props.messages[props.messages.length - 1]._id + ); + } + }, [props.messages.length]); + + const handleScroll = useCallback(() => { + if (containerRef.current?.scrollTop === 0) { + onUpdateReadedMessageRef.current( + props.messages[props.messages.length - 1]._id + ); + } + }, [props.messages]); + return (
{props.messages.map((message, index, arr) => { diff --git a/web/src/components/ChatBox/index.tsx b/web/src/components/ChatBox/index.tsx index 6dfccce8..9d0d0fe7 100644 --- a/web/src/components/ChatBox/index.tsx +++ b/web/src/components/ChatBox/index.tsx @@ -4,6 +4,7 @@ import { useConverseMessage } from 'tailchat-shared'; import { AlertErrorView } from '../AlertErrorView'; import { ChatInputBox } from './ChatInputBox'; import { ChatMessageList, ChatMessageListRef } from './ChatMessageList'; +import { useMessageAck } from './useMessageAck'; const ChatBoxPlaceholder: React.FC = React.memo(() => { return ( @@ -41,6 +42,7 @@ export const ChatBox: React.FC = React.memo((props) => { isGroup, }); const chatMessageListRef = useRef(null); + const { updateConverseAck } = useMessageAck(converseId, messages); if (loading) { return ; @@ -52,10 +54,15 @@ export const ChatBox: React.FC = React.memo((props) => { return (
- + { + // 发送消息后滚动到底部 handleSendMessage({ converseId: props.converseId, groupId: props.groupId, diff --git a/web/src/components/ChatBox/useMessageAck.ts b/web/src/components/ChatBox/useMessageAck.ts new file mode 100644 index 00000000..5c4927bc --- /dev/null +++ b/web/src/components/ChatBox/useMessageAck.ts @@ -0,0 +1,61 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { + ChatMessage, + isValidStr, + updateAck, + useAppDispatch, + useUpdateRef, +} from 'tailchat-shared'; +import { chatActions } from 'tailchat-shared/redux/slices'; +import _debounce from 'lodash/debounce'; + +export function useMessageAck(converseId: string, messages: ChatMessage[]) { + const messagesRef = useUpdateRef(messages); + const dispatch = useAppDispatch(); + const lastMessageIdRef = useRef(''); + + 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: false, trailing: true } + ), + [] + ); + + useEffect(() => { + // 设置当前 + if (messagesRef.current.length === 0) { + return; + } + + const lastMessageId = + messagesRef.current[messagesRef.current.length - 1]._id; + setConverseAck(converseId, lastMessageId); + }, [converseId]); + + /** + * 更新会话最新消息 + */ + const updateConverseAck = useCallback( + (lastMessageId: string) => { + setConverseAck(converseId, lastMessageId); + }, + [converseId] + ); + + return { updateConverseAck }; +}