Mark messages read on delay when timeline becomes visible

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2025-07-22 10:11:27 -05:00 committed by GitHub
parent 1f10105b22
commit da1777924b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 29 additions and 5 deletions

View file

@ -462,6 +462,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
isBlocked: false, isBlocked: false,
isConversationSelected: true, isConversationSelected: true,
isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false, isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false,
isInFullScreenCall: false,
items: overrideProps.items ?? Object.keys(items), items: overrideProps.items ?? Object.keys(items),
messageChangeCounter: 0, messageChangeCounter: 0,
messageLoadingState: null, messageLoadingState: null,

View file

@ -40,7 +40,7 @@ import {
setScrollBottom, setScrollBottom,
} from '../../util/scrollUtil'; } from '../../util/scrollUtil';
import { LastSeenIndicator } from './LastSeenIndicator'; import { LastSeenIndicator } from './LastSeenIndicator';
import { MINUTE } from '../../util/durations'; import { MINUTE, SECOND } from '../../util/durations';
import { SizeObserver } from '../../hooks/useSizeObserver'; import { SizeObserver } from '../../hooks/useSizeObserver';
import { import {
createScrollerLock, createScrollerLock,
@ -54,6 +54,8 @@ const MIN_ROW_HEIGHT = 18;
const SCROLL_DOWN_BUTTON_THRESHOLD = 8; const SCROLL_DOWN_BUTTON_THRESHOLD = 8;
const LOAD_NEWER_THRESHOLD = 5; const LOAD_NEWER_THRESHOLD = 5;
const DELAY_BEFORE_MARKING_READ_AFTER_FOCUS = SECOND;
export type WarningType = ReadonlyDeep< export type WarningType = ReadonlyDeep<
| { | {
type: ContactSpoofingType.DirectConversationWithSameTitle; type: ContactSpoofingType.DirectConversationWithSameTitle;
@ -84,6 +86,7 @@ type PropsHousekeepingType = {
isBlocked: boolean; isBlocked: boolean;
isConversationSelected: boolean; isConversationSelected: boolean;
isGroupV1AndDisabled?: boolean; isGroupV1AndDisabled?: boolean;
isInFullScreenCall: boolean;
isIncomingMessageRequest: boolean; isIncomingMessageRequest: boolean;
isSomeoneTyping: boolean; isSomeoneTyping: boolean;
unreadCount?: number; unreadCount?: number;
@ -517,6 +520,17 @@ export class Timeline extends React.Component<
} }
}, 500); }, 500);
// When the the window becomes active, or when a fullsceen call is ended, we mark read
// with a delay, to allow users to navigate away quickly without marking messages read
#markNewestBottomVisibleMessageReadAfterDelay = throttle(
this.#markNewestBottomVisibleMessageRead,
DELAY_BEFORE_MARKING_READ_AFTER_FOCUS,
{
leading: false,
trailing: true,
}
);
#setupGroupCallPeekTimeouts(): void { #setupGroupCallPeekTimeouts(): void {
this.#cleanupGroupCallPeekTimeouts(); this.#cleanupGroupCallPeekTimeouts();
@ -558,7 +572,7 @@ export class Timeline extends React.Component<
this.#updateIntersectionObserver(); this.#updateIntersectionObserver();
window.SignalContext.activeWindowService.registerForActive( window.SignalContext.activeWindowService.registerForActive(
this.#markNewestBottomVisibleMessageRead this.#markNewestBottomVisibleMessageReadAfterDelay
); );
if (conversationType === 'group') { if (conversationType === 'group') {
@ -568,9 +582,10 @@ export class Timeline extends React.Component<
public override componentWillUnmount(): void { public override componentWillUnmount(): void {
window.SignalContext.activeWindowService.unregisterForActive( window.SignalContext.activeWindowService.unregisterForActive(
this.#markNewestBottomVisibleMessageRead this.#markNewestBottomVisibleMessageReadAfterDelay
); );
this.#markNewestBottomVisibleMessageReadAfterDelay.cancel();
this.#markNewestBottomVisibleMessageRead.cancel();
this.#intersectionObserver?.disconnect(); this.#intersectionObserver?.disconnect();
this.#cleanupGroupCallPeekTimeouts(); this.#cleanupGroupCallPeekTimeouts();
this.props.updateVisibleMessages?.([]); this.props.updateVisibleMessages?.([]);
@ -625,6 +640,7 @@ export class Timeline extends React.Component<
): void { ): void {
const { const {
conversationType: previousConversationType, conversationType: previousConversationType,
isInFullScreenCall: previousIsInFullScreenCall,
items: oldItems, items: oldItems,
messageChangeCounter: previousMessageChangeCounter, messageChangeCounter: previousMessageChangeCounter,
messageLoadingState: previousMessageLoadingState, messageLoadingState: previousMessageLoadingState,
@ -633,6 +649,7 @@ export class Timeline extends React.Component<
conversationType, conversationType,
discardMessages, discardMessages,
id, id,
isInFullScreenCall,
items: newItems, items: newItems,
messageChangeCounter, messageChangeCounter,
messageLoadingState, messageLoadingState,
@ -705,6 +722,10 @@ export class Timeline extends React.Component<
this.#markNewestBottomVisibleMessageRead(); this.#markNewestBottomVisibleMessageRead();
} }
if (previousIsInFullScreenCall && !isInFullScreenCall) {
this.#markNewestBottomVisibleMessageReadAfterDelay();
}
if (previousConversationType !== conversationType) { if (previousConversationType !== conversationType) {
this.#cleanupGroupCallPeekTimeouts(); this.#cleanupGroupCallPeekTimeouts();
if (conversationType === 'group') { if (conversationType === 'group') {

View file

@ -43,6 +43,7 @@ import { SmartMiniPlayer } from './MiniPlayer';
import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem'; import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem';
import { SmartTypingBubble } from './TypingBubble'; import { SmartTypingBubble } from './TypingBubble';
import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager'; import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager';
import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling';
type ExternalProps = { type ExternalProps = {
id: string; id: string;
@ -166,7 +167,7 @@ export const SmartTimeline = memo(function SmartTimeline({
const selectedConversationId = useSelector(getSelectedConversationId); const selectedConversationId = useSelector(getSelectedConversationId);
const targetedMessage = useSelector(getTargetedMessage); const targetedMessage = useSelector(getTargetedMessage);
const theme = useSelector(getTheme); const theme = useSelector(getTheme);
const isInFullScreenCall = useSelector(getIsInFullScreenCall);
const conversation = conversationSelector(id); const conversation = conversationSelector(id);
const conversationMessages = conversationMessagesSelector(id); const conversationMessages = conversationMessagesSelector(id);
@ -257,6 +258,7 @@ export const SmartTimeline = memo(function SmartTimeline({
isBlocked={isBlocked} isBlocked={isBlocked}
isConversationSelected={isConversationSelected} isConversationSelected={isConversationSelected}
isGroupV1AndDisabled={isGroupV1AndDisabled} isGroupV1AndDisabled={isGroupV1AndDisabled}
isInFullScreenCall={isInFullScreenCall}
isIncomingMessageRequest={isIncomingMessageRequest} isIncomingMessageRequest={isIncomingMessageRequest}
isNearBottom={isNearBottom} isNearBottom={isNearBottom}
isSomeoneTyping={isSomeoneTyping} isSomeoneTyping={isSomeoneTyping}