From da1777924b049bf383c4b71140e1dd20b15b90b3 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:11:27 -0500 Subject: [PATCH] Mark messages read on delay when timeline becomes visible Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com> --- .../conversation/Timeline.stories.tsx | 1 + ts/components/conversation/Timeline.tsx | 29 ++++++++++++++++--- ts/state/smart/Timeline.tsx | 4 ++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 427f9f8a1f..083d41e0f0 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -462,6 +462,7 @@ const useProps = (overrideProps: Partial = {}): PropsType => ({ isBlocked: false, isConversationSelected: true, isIncomingMessageRequest: overrideProps.isIncomingMessageRequest ?? false, + isInFullScreenCall: false, items: overrideProps.items ?? Object.keys(items), messageChangeCounter: 0, messageLoadingState: null, diff --git a/ts/components/conversation/Timeline.tsx b/ts/components/conversation/Timeline.tsx index c55f4427ec..0d92fa1e66 100644 --- a/ts/components/conversation/Timeline.tsx +++ b/ts/components/conversation/Timeline.tsx @@ -40,7 +40,7 @@ import { setScrollBottom, } from '../../util/scrollUtil'; import { LastSeenIndicator } from './LastSeenIndicator'; -import { MINUTE } from '../../util/durations'; +import { MINUTE, SECOND } from '../../util/durations'; import { SizeObserver } from '../../hooks/useSizeObserver'; import { createScrollerLock, @@ -54,6 +54,8 @@ const MIN_ROW_HEIGHT = 18; const SCROLL_DOWN_BUTTON_THRESHOLD = 8; const LOAD_NEWER_THRESHOLD = 5; +const DELAY_BEFORE_MARKING_READ_AFTER_FOCUS = SECOND; + export type WarningType = ReadonlyDeep< | { type: ContactSpoofingType.DirectConversationWithSameTitle; @@ -84,6 +86,7 @@ type PropsHousekeepingType = { isBlocked: boolean; isConversationSelected: boolean; isGroupV1AndDisabled?: boolean; + isInFullScreenCall: boolean; isIncomingMessageRequest: boolean; isSomeoneTyping: boolean; unreadCount?: number; @@ -517,6 +520,17 @@ export class Timeline extends React.Component< } }, 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 { this.#cleanupGroupCallPeekTimeouts(); @@ -558,7 +572,7 @@ export class Timeline extends React.Component< this.#updateIntersectionObserver(); window.SignalContext.activeWindowService.registerForActive( - this.#markNewestBottomVisibleMessageRead + this.#markNewestBottomVisibleMessageReadAfterDelay ); if (conversationType === 'group') { @@ -568,9 +582,10 @@ export class Timeline extends React.Component< public override componentWillUnmount(): void { window.SignalContext.activeWindowService.unregisterForActive( - this.#markNewestBottomVisibleMessageRead + this.#markNewestBottomVisibleMessageReadAfterDelay ); - + this.#markNewestBottomVisibleMessageReadAfterDelay.cancel(); + this.#markNewestBottomVisibleMessageRead.cancel(); this.#intersectionObserver?.disconnect(); this.#cleanupGroupCallPeekTimeouts(); this.props.updateVisibleMessages?.([]); @@ -625,6 +640,7 @@ export class Timeline extends React.Component< ): void { const { conversationType: previousConversationType, + isInFullScreenCall: previousIsInFullScreenCall, items: oldItems, messageChangeCounter: previousMessageChangeCounter, messageLoadingState: previousMessageLoadingState, @@ -633,6 +649,7 @@ export class Timeline extends React.Component< conversationType, discardMessages, id, + isInFullScreenCall, items: newItems, messageChangeCounter, messageLoadingState, @@ -705,6 +722,10 @@ export class Timeline extends React.Component< this.#markNewestBottomVisibleMessageRead(); } + if (previousIsInFullScreenCall && !isInFullScreenCall) { + this.#markNewestBottomVisibleMessageReadAfterDelay(); + } + if (previousConversationType !== conversationType) { this.#cleanupGroupCallPeekTimeouts(); if (conversationType === 'group') { diff --git a/ts/state/smart/Timeline.tsx b/ts/state/smart/Timeline.tsx index b30403a18e..5a7f3fa9f3 100644 --- a/ts/state/smart/Timeline.tsx +++ b/ts/state/smart/Timeline.tsx @@ -43,6 +43,7 @@ import { SmartMiniPlayer } from './MiniPlayer'; import { SmartTimelineItem, type SmartTimelineItemProps } from './TimelineItem'; import { SmartTypingBubble } from './TypingBubble'; import { AttachmentDownloadManager } from '../../jobs/AttachmentDownloadManager'; +import { isInFullScreenCall as getIsInFullScreenCall } from '../selectors/calling'; type ExternalProps = { id: string; @@ -166,7 +167,7 @@ export const SmartTimeline = memo(function SmartTimeline({ const selectedConversationId = useSelector(getSelectedConversationId); const targetedMessage = useSelector(getTargetedMessage); const theme = useSelector(getTheme); - + const isInFullScreenCall = useSelector(getIsInFullScreenCall); const conversation = conversationSelector(id); const conversationMessages = conversationMessagesSelector(id); @@ -257,6 +258,7 @@ export const SmartTimeline = memo(function SmartTimeline({ isBlocked={isBlocked} isConversationSelected={isConversationSelected} isGroupV1AndDisabled={isGroupV1AndDisabled} + isInFullScreenCall={isInFullScreenCall} isIncomingMessageRequest={isIncomingMessageRequest} isNearBottom={isNearBottom} isSomeoneTyping={isSomeoneTyping}