refactor: 优化已读逻辑,修改为使用Intersection来校验消息是否已读

pull/56/head
moonrailgun 2 years ago
parent db4930e36d
commit da63c2b775

@ -30,6 +30,11 @@ export interface SharedEventMap {
* null
*/
replyMessage: (payload: ChatMessage | null) => void;
/**
* ()
*/
readMessage: (payload: ChatMessage | null) => void;
}
export type SharedEventType = keyof SharedEventMap;

@ -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 };
}

@ -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;
}

@ -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)}
</Divider>
)}
<ChatMessageItem showAvatar={showAvatar} payload={message} />
<Intersection
onIntersection={() => sharedEvent.emit('readMessage', message)}
>
<ChatMessageItem showAvatar={showAvatar} payload={message} />
</Intersection>
</div>
);
}

@ -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<MessageListProps> = React.memo(
(props) => {
const containerRef = useRef<HTMLDivElement>(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(() => {

@ -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<MessageListProps> = 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({

@ -4,6 +4,5 @@ export interface MessageListProps {
messages: ChatMessage[];
isLoadingMore: boolean;
hasMoreMessage: boolean;
onUpdateReadedMessage: (lastMessageId: string) => void;
onLoadMore: () => Promise<void>;
}

@ -33,7 +33,7 @@ const ChatBoxInner: React.FC<ChatBoxProps> = React.memo((props) => {
converseId,
isGroup,
});
const { updateConverseAck } = useMessageAck(converseId, messages);
useMessageAck(converseId);
if (loading) {
return <ChatBoxPlaceholder />;
@ -50,7 +50,6 @@ const ChatBoxInner: React.FC<ChatBoxProps> = React.memo((props) => {
messages={messages}
isLoadingMore={isLoadingMore}
hasMoreMessage={hasMoreMessage}
onUpdateReadedMessage={updateConverseAck}
onLoadMore={handleFetchMoreMessage}
/>

@ -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]);
}

@ -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<IntersectionProps> = React.memo((props) => {
const ref = useRef<HTMLDivElement>(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 <div ref={ref}>{props.children}</div>;
});
Intersection.displayName = 'Intersection';
Loading…
Cancel
Save