From 52781cfa2d0a2f230bff9ce8ed2d1501b2dffa16 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 24 Aug 2026 12:32:44 +0200 Subject: [PATCH] refactor(app): drop the unread jump anchor now the scroller finds it (#1858) --- .../app/hooks/useChannelHoverPreload.ts | 4 +- .../handlers/defaultHandlers.ts | 2 +- .../channel/components/ChannelMessages.tsx | 8 +- .../features/guild/state/GuildReadState.ts | 2 +- .../navigation/state/NavigationSideEffects.ts | 2 - .../notification/state/NotificationAutoAck.ts | 4 +- .../features/read_state/state/ReadStates.ts | 74 ++++++------ .../read_states/ReadStateAckMachine.test.ts | 22 ++-- .../state/read_states/ReadStateAckMachine.ts | 12 +- .../state/read_states/ReadStateEntry.ts | 110 +++++++++--------- .../ReadStateEntryStatusMachine.bench.ts | 14 +-- .../ReadStateEntryStatusMachine.test.ts | 20 ++-- .../ReadStateEntryStatusMachine.ts | 16 +-- .../read_states/ReadStateServerAckMachine.ts | 4 +- .../read_state/state/read_states/shared.ts | 2 +- .../state/QuickSwitcherResultGenerators.ts | 2 +- .../action_menu/FluxerButtonContextMenu.tsx | 2 +- 17 files changed, 148 insertions(+), 152 deletions(-) diff --git a/fluxer_app/src/features/app/hooks/useChannelHoverPreload.ts b/fluxer_app/src/features/app/hooks/useChannelHoverPreload.ts index 9c7526b46..05f96753a 100644 --- a/fluxer_app/src/features/app/hooks/useChannelHoverPreload.ts +++ b/fluxer_app/src/features/app/hooks/useChannelHoverPreload.ts @@ -4,7 +4,6 @@ import type {Channel} from '@app/features/channel/models/Channel'; import type {Guild} from '@app/features/guild/models/Guild'; import {ensureMembersForMessages} from '@app/features/messaging/commands/MessageCommands'; import Messages from '@app/features/messaging/state/MessagingMessages'; -import ReadStates from '@app/features/read_state/state/ReadStates'; import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants'; import {useCallback, useEffect, useRef} from 'react'; @@ -57,8 +56,7 @@ export function useChannelHoverPreload({ return; } if (preloadMessages) { - const unreadAnchor = ReadStates.getUnreadJumpAnchor(channel.id); - if (!Messages.preloadLatestPage(channel.id, guild?.id ?? channel.guildId ?? null, unreadAnchor)) { + if (!Messages.preloadLatestPage(channel.id, guild?.id ?? channel.guildId ?? null)) { ensureMembersForCachedChannelMessages(channel.id); } } diff --git a/fluxer_app/src/features/app/keybindings/keybind_manager/handlers/defaultHandlers.ts b/fluxer_app/src/features/app/keybindings/keybind_manager/handlers/defaultHandlers.ts index a3e278368..1a94e1281 100644 --- a/fluxer_app/src/features/app/keybindings/keybind_manager/handlers/defaultHandlers.ts +++ b/fluxer_app/src/features/app/keybindings/keybind_manager/handlers/defaultHandlers.ts @@ -349,7 +349,7 @@ export function registerDefaultKeybindHandlers(host: HandlerHost, i18n: I18n): v if (type !== 'press') return; const inboxTab = Inbox.selectedTab; if (inboxTab === 'unreadChannels') { - const unreadChannelId = ReadStates.getChannelIds().find((channelId) => ReadStates.hasUnreadOrMentions(channelId)); + const unreadChannelId = ReadStates.getChannelIds().find((channelId) => ReadStates.isUnreadOrMentioned(channelId)); if (unreadChannelId) { ReadStateCommands.ack(unreadChannelId, true, true); } diff --git a/fluxer_app/src/features/channel/components/ChannelMessages.tsx b/fluxer_app/src/features/channel/components/ChannelMessages.tsx index 08e503ef7..4be6cb332 100644 --- a/fluxer_app/src/features/channel/components/ChannelMessages.tsx +++ b/fluxer_app/src/features/channel/components/ChannelMessages.tsx @@ -102,7 +102,7 @@ interface MessagesStateSnapshot { editingMessageId: string | null; currentUser: User | undefined; isEstimated: boolean; - isManualAck: boolean; + ackedManually: boolean; } interface MessagesProps { @@ -132,7 +132,7 @@ const readFromState = (channelId: string): MessagesStateSnapshot => { editingMessageId: MessageEdit.getEditingMessageId(channelId), currentUser: Users.currentUser ?? undefined, isEstimated: ReadStates.getIfExists(channelId)?.estimated ?? false, - isManualAck: ReadStates.getIfExists(channelId)?.isManualAck ?? false, + ackedManually: ReadStates.getIfExists(channelId)?.ackedManually ?? false, }; }; @@ -185,7 +185,7 @@ export const Messages = observer(function Messages({ windowFocused: isWindowFocused, atBottom: true, textChatVisible: true, - manualAck: state.isManualAck, + manualAck: state.ackedManually, blockingModalOpen: isModalOpen || MediaViewer.isOpen, }); const jumpHighlightTimeoutRef = useRef(null); @@ -498,7 +498,7 @@ export const Messages = observer(function Messages({ useEffect(() => { return () => { const readState = ReadStates.getIfExists(channel.id); - if (readState?.isManualAck) { + if (readState?.ackedManually) { ReadStateCommands.clearManualAck(channel.id); } ReadStateCommands.clearStickyUnread(channel.id); diff --git a/fluxer_app/src/features/guild/state/GuildReadState.ts b/fluxer_app/src/features/guild/state/GuildReadState.ts index c6ac25a72..5b950abd7 100644 --- a/fluxer_app/src/features/guild/state/GuildReadState.ts +++ b/fluxer_app/src/features/guild/state/GuildReadState.ts @@ -379,7 +379,7 @@ class GuildReadState { if (!GUILD_TEXT_BASED_CHANNEL_TYPES.has(channel.type)) { continue; } - if (ReadStates.hasUnreadOrMentions(channel.id)) { + if (ReadStates.isUnreadOrMentioned(channel.id)) { return true; } } diff --git a/fluxer_app/src/features/navigation/state/NavigationSideEffects.ts b/fluxer_app/src/features/navigation/state/NavigationSideEffects.ts index 15ae201b3..08ac7ea44 100644 --- a/fluxer_app/src/features/navigation/state/NavigationSideEffects.ts +++ b/fluxer_app/src/features/navigation/state/NavigationSideEffects.ts @@ -3,7 +3,6 @@ import Messages from '@app/features/messaging/state/MessagingMessages'; import Navigation from '@app/features/navigation/state/Navigation'; import {Logger} from '@app/features/platform/utils/AppLogger'; -import ReadStates from '@app/features/read_state/state/ReadStates'; import Notification from '@app/features/ui/state/Notification'; import {reaction} from 'mobx'; @@ -41,7 +40,6 @@ class NavigationSideEffects { guildId: guildId ?? undefined, channelId, messageId: messageId ?? undefined, - unreadAnchor: ReadStates.getUnreadJumpAnchor(channelId), }); Notification.handleChannelSelect({channelId}); } diff --git a/fluxer_app/src/features/notification/state/NotificationAutoAck.ts b/fluxer_app/src/features/notification/state/NotificationAutoAck.ts index 18d943cc9..9264d0301 100644 --- a/fluxer_app/src/features/notification/state/NotificationAutoAck.ts +++ b/fluxer_app/src/features/notification/state/NotificationAutoAck.ts @@ -50,7 +50,7 @@ class AutoAck { } const isAtBottom = Dimension.isAtBottom(channelId) ?? false; const readState = ReadStates.getIfExists(channelId); - const isManualAck = readState?.isManualAck ?? false; + const ackedManually = readState?.ackedManually ?? false; const channel = Channels.getChannel(channelId); const isGuildVoiceCallExpanded = channel?.type === ChannelTypes.GUILD_VOICE @@ -63,7 +63,7 @@ class AutoAck { activeVoiceCallFullscreenScopeKey: VoiceCallFullscreen.activeScopeKey, }); const isMediaViewerOpen = MediaViewer.isOpen; - const canAutoAck = !isManualAck && isWindowFocused && isTextChatVisible && !isMediaViewerOpen; + const canAutoAck = !ackedManually && isWindowFocused && isTextChatVisible && !isMediaViewerOpen; return {windowId, channelId, isAtBottom, canAutoAck}; }, (conditions) => { diff --git a/fluxer_app/src/features/read_state/state/ReadStates.ts b/fluxer_app/src/features/read_state/state/ReadStates.ts index 21d0bbe22..be1905757 100644 --- a/fluxer_app/src/features/read_state/state/ReadStates.ts +++ b/fluxer_app/src/features/read_state/state/ReadStates.ts @@ -50,10 +50,10 @@ import {observable, runInAction} from 'mobx'; export type {GatewayReadState}; -const logger = new Logger('ReadStates'); - export type {UnreadJumpAnchor}; +const logger = new Logger('ReadStates'); + class ReadStates { private readonly states = new Map(); private readonly archivedStates = new Map(); @@ -128,7 +128,7 @@ class ReadStates { private refreshMentionChannel(channelId: string): void { const state = this.states.get(channelId as ChannelId); - if (state != null && state.mentionCount > 0 && state.canHaveMentions()) { + if (state != null && state.mentionCount > 0 && state.supportsMentions()) { this.mentionChannels.add(channelId as ChannelId); } else { this.mentionChannels.delete(channelId as ChannelId); @@ -243,7 +243,7 @@ class ReadStates { } this.archivedStates.set(channelId as ChannelId, { ackMessageId: entry.ackMessageId, - ackPinTimestamp: entry.ackPinTimestamp, + acknowledgedPinTimestamp: entry.acknowledgedPinTimestamp, readStateKnown: entry.readStateKnown, }); } @@ -267,7 +267,7 @@ class ReadStates { const ids: Array = []; for (const channelId of Array.from(this.mentionChannels)) { const state = this.getIfExists(channelId); - if (state?.canHaveMentions()) { + if (state?.supportsMentions()) { ids.push(channelId); } else { this.mentionChannels.delete(channelId); @@ -290,7 +290,7 @@ class ReadStates { getMentionCount(channelId: string): number { this.versionBox.get(); const state = this.getIfExists(channelId); - if (state == null || !state.canHaveMentions()) return 0; + if (state == null || !state.supportsMentions()) return 0; return state.mentionCount; } @@ -321,14 +321,14 @@ class ReadStates { getPrivateChannelMentionCount(channelId: string): number { this.privateChannelVersionBox.get(); const state = this.getIfExists(channelId); - if (state == null || !state.canHaveMentions()) return 0; + if (state == null || !state.supportsMentions()) return 0; return state.mentionCount; } - hasUnreadOrMentions(channelId: string): boolean { + isUnreadOrMentioned(channelId: string): boolean { this.versionBox.get(); const state = this.getIfExists(channelId); - return !!(state?.canBeUnread() && state.hasUnreadOrMentions()); + return !!(state?.canBeUnread() && state.isUnreadOrMentioned()); } ackMessageId(channelId: string): string | null { @@ -343,12 +343,12 @@ class ReadStates { getOldestUnreadMessageId(channelId: string): string | null { const state = this.getIfExists(channelId); - return state?.canTrackUnreads() ? state.oldestUnreadMessageId : null; + return state?.supportsUnreadTracking() ? state.oldestUnreadMessageId : null; } getVisualUnreadMessageId(channelId: string): string | null { const state = this.getIfExists(channelId); - return state?.canTrackUnreads() ? state.visualUnreadMessageId : null; + return state?.supportsUnreadTracking() ? state.visualUnreadMessageId : null; } getUnreadJumpAnchor(channelId: string): UnreadJumpAnchor | null { @@ -358,7 +358,7 @@ class ReadStates { } return resolveUnreadJumpAnchor({ canBeUnread: state.canBeUnread(), - canTrackUnreads: state.canTrackUnreads(), + canTrackUnreads: state.supportsUnreadTracking(), hasUnread: state.hasUnread(), oldestUnreadMessageId: state.oldestUnreadMessageId, ackMessageId: state.ackMessageId, @@ -395,7 +395,7 @@ class ReadStates { hasUnreadPins(channelId: string): boolean { const state = this.getIfExists(channelId); - return !!(state?.canBeUnread() && state.lastPinTimestamp > state.ackPinTimestamp); + return !!(state?.canBeUnread() && state.lastPinTimestamp > state.acknowledgedPinTimestamp); } ackPins(channelId: string): void { @@ -424,7 +424,7 @@ class ReadStates { state.readStateKnown = true; this.setMentionCount(state, readState.mention_count ?? 0); state.ackMessageId = readState.last_message_id ?? null; - state.ackPinTimestamp = parseTimestamp(readState.last_pin_timestamp); + state.acknowledgedPinTimestamp = parseTimestamp(readState.last_pin_timestamp); state.serverVersion = readState.version ?? null; } for (const channel of action.channels) { @@ -432,7 +432,7 @@ class ReadStates { const state = this.get(channel.id); state.lastMessageId = channel.last_message_id ?? null; state.lastPinTimestamp = parseTimestamp(channel.last_pin_timestamp); - state._guildId = channel.guild_id ?? null; + state.storedGuildId = channel.guild_id ?? null; if (!channelsWithReadState.has(channel.id as ChannelId)) { this.setMentionCount(state, 0); } @@ -455,7 +455,7 @@ class ReadStates { const state = this.get(channel.id); state.lastMessageId = channel.last_message_id ?? null; state.lastPinTimestamp = parseTimestamp(channel.last_pin_timestamp); - state._guildId = action.guild.id; + state.storedGuildId = action.guild.id; this.refreshUnreadEstimate(state); this.refreshMentionChannel(channel.id); } @@ -466,7 +466,7 @@ class ReadStates { handleLoadMessages(action: {channelId: string; isAfter?: boolean; messages: Array}): void { const state = this.get(action.channelId); - state.loadedMessages = true; + state.messagesLoaded = true; const messages = Messages.getMessages(action.channelId); const newestMessage = messages.last(); if (newestMessage != null && isNewerMessageId(newestMessage.id, state.lastMessageId)) { @@ -484,12 +484,12 @@ class ReadStates { handleIncomingMessage(action: {channelId: string; message: WireMessage}): void { const state = this.get(action.channelId); if (action.message.guild_id != null) { - state._guildId = action.message.guild_id; + state.storedGuildId = action.message.guild_id; } const previousLastMessageId = state.lastMessageId; const currentUser = Users.getCurrentUser(); const authorBlocked = Relationships.isBlocked(action.message.author.id); - const hadUnreadOrMentions = state.hasUnreadOrMentions(); + const hadUnreadOrMentions = state.isUnreadOrMentioned(); if (isNewerMessageId(action.message.id, state.lastMessageId)) { state.lastMessageId = action.message.id; } @@ -540,7 +540,7 @@ class ReadStates { state.ackMessageId = previousLastMessageId; state.readStateKnown = true; } - if (state.oldestUnreadMessageId == null || state.oldestUnreadMessageIdStale) { + if (state.oldestUnreadMessageId == null || state.oldestUnreadNeedsRecompute) { state.oldestUnreadMessageId = action.message.id; } state.unreadCount++; @@ -563,11 +563,11 @@ class ReadStates { const state = this.get(action.channel.id); state.lastMessageId = action.channel.last_message_id ?? null; state.lastPinTimestamp = parseTimestamp(action.channel.last_pin_timestamp); - state._guildId = action.channel.guild_id ?? null; + state.storedGuildId = action.channel.guild_id ?? null; const archivedState = this.archivedStates.get(action.channel.id as ChannelId); if (archivedState != null) { state.ackMessageId = archivedState.ackMessageId; - state.ackPinTimestamp = archivedState.ackPinTimestamp; + state.acknowledgedPinTimestamp = archivedState.acknowledgedPinTimestamp; state.readStateKnown = archivedState.readStateKnown; this.archivedStates.delete(action.channel.id as ChannelId); } @@ -600,7 +600,7 @@ class ReadStates { let changed = false; if (guildId != null) { changed = state.guildId !== guildId; - state._guildId = guildId; + state.storedGuildId = guildId; } if (isNewerMessageId(lastMessageId, state.lastMessageId)) { state.lastMessageId = lastMessageId; @@ -746,7 +746,7 @@ class ReadStates { case 'applyManualAck': state.readStateKnown = true; state.clearStickyUnread(); - state.isManualAck = true; + state.ackedManually = true; state.rebuild(action.messageId, {recomputeMentions: true}); state.serverVersion = action.version ?? state.serverVersion; this.cancelPendingAck(action.channelId); @@ -793,15 +793,15 @@ class ReadStates { handleClearManualAck(action: {channelId: string}): void { const state = this.get(action.channelId); - if (state.isManualAck) { - state.isManualAck = false; + if (state.ackedManually) { + state.ackedManually = false; this.notifyChange(action.channelId); } } handleRelationshipUpdate(): void { for (const state of this.states.values()) { - if (state.hasUnreadOrMentions()) { + if (state.isUnreadOrMentioned()) { state.rebuild(undefined, {recomputeMentions: true}); } } @@ -832,9 +832,9 @@ class ReadStates { requestedMessageId: messageId, lastMessageId: state.lastMessageId, ackMessageId: state.ackMessageId, - isManualAck: state.isManualAck, - loadedMessages: state.loadedMessages, - canTrackUnreads: state.canTrackUnreads(), + ackedManually: state.ackedManually, + messagesLoaded: state.messagesLoaded, + supportsUnreadTracking: state.supportsUnreadTracking(), hasMentions: state.hasMentions(), hasOldestUnreadMessage: state.oldestUnreadMessageId != null, hasStickyUnreadMessage: state.stickyUnreadMessageId != null, @@ -856,7 +856,7 @@ class ReadStates { state.ackMessageId = decision.messageId; state.oldestUnreadMessageId = null; if (decision.shouldClearManualAck) { - state.isManualAck = false; + state.ackedManually = false; state.clearStickyUnread(); } this.cancelPendingAckIfCovered(state.channelId, decision.messageId); @@ -866,10 +866,10 @@ class ReadStates { private applyPinAck(state: ReadStateEntry, timestamp?: string | null): boolean { const newTimestamp = timestamp == null ? state.lastPinTimestamp : parseTimestamp(timestamp); const ackTimestamp = newTimestamp !== 0 ? newTimestamp : state.lastPinTimestamp; - if (state.ackPinTimestamp === ackTimestamp) { + if (state.acknowledgedPinTimestamp === ackTimestamp) { return false; } - state.ackPinTimestamp = ackTimestamp; + state.acknowledgedPinTimestamp = ackTimestamp; return true; } @@ -895,7 +895,7 @@ class ReadStates { this.pendingAcks.set(channelId as ChannelId, pending); const state = this.getIfExists(channelId); if (state != null) { - state.outgoingAck = pending.messageId; + state.inFlightAckMessageId = pending.messageId; } this.scheduleAckFlush(); } @@ -938,7 +938,7 @@ class ReadStates { const deleted = this.pendingAcks.delete(channelId as ChannelId); const state = this.getIfExists(channelId); if (state != null) { - state.outgoingAck = null; + state.inFlightAckMessageId = null; } if (deleted) { this.scheduleAckFlush(); @@ -988,8 +988,8 @@ class ReadStates { if (current?.messageId === entry.messageId) { this.pendingAcks.delete(entry.channelId as ChannelId); const state = this.getIfExists(entry.channelId); - if (state?.outgoingAck === entry.messageId) { - state.outgoingAck = null; + if (state?.inFlightAckMessageId === entry.messageId) { + state.inFlightAckMessageId = null; } } } diff --git a/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.test.ts b/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.test.ts index aa6946c0b..f93672884 100644 --- a/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.test.ts +++ b/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.test.ts @@ -20,9 +20,9 @@ function input(overrides: Partial = {}): ReadStateAckInput { requestedMessageId: NEWER_ID, lastMessageId: NEWER_ID, ackMessageId: ACK_ID, - isManualAck: false, - loadedMessages: true, - canTrackUnreads: true, + ackedManually: false, + messagesLoaded: true, + supportsUnreadTracking: true, hasMentions: false, hasOldestUnreadMessage: false, hasStickyUnreadMessage: false, @@ -53,30 +53,30 @@ describe('readStateAckMachine', () => { }); it('blocks automatic ack while manual ack is held, messages are unloaded, or the channel is untracked', () => { - expect(resolveReadStateAckDecision(input({isManualAck: true}))).toEqual({ + expect(resolveReadStateAckDecision(input({ackedManually: true}))).toEqual({ type: 'ignored', reason: 'manualAck', }); - expect(resolveReadStateAckDecision(input({loadedMessages: false}))).toEqual({ + expect(resolveReadStateAckDecision(input({messagesLoaded: false}))).toEqual({ type: 'ignored', reason: 'notLoaded', }); - expect(resolveReadStateAckDecision(input({canTrackUnreads: false}))).toEqual({ + expect(resolveReadStateAckDecision(input({supportsUnreadTracking: false}))).toEqual({ type: 'ignored', reason: 'untracked', }); }); it('lets local, forced, and explicit user acknowledgements override automatic ack guards', () => { - expect(resolveReadStateAckDecision(input({isManualAck: true, isExplicitUserAction: true}))).toMatchObject({ + expect(resolveReadStateAckDecision(input({ackedManually: true, isExplicitUserAction: true}))).toMatchObject({ type: 'ack', shouldClearManualAck: true, }); - expect(resolveReadStateAckDecision(input({loadedMessages: false, local: true}))).toMatchObject({ + expect(resolveReadStateAckDecision(input({messagesLoaded: false, local: true}))).toMatchObject({ type: 'ack', shouldClearManualAck: true, }); - expect(resolveReadStateAckDecision(input({canTrackUnreads: false, force: true}))).toMatchObject({ + expect(resolveReadStateAckDecision(input({supportsUnreadTracking: false, force: true}))).toMatchObject({ type: 'ack', shouldClearManualAck: true, }); @@ -125,12 +125,12 @@ describe('readStateAckMachine', () => { }); it('updates the decision from later ack input', () => { - const ignoredSnapshot = createReadStateAckSnapshot(input({isManualAck: true})); + const ignoredSnapshot = createReadStateAckSnapshot(input({ackedManually: true})); expect(selectReadStateAckDecision(ignoredSnapshot)).toEqual({type: 'ignored', reason: 'manualAck'}); const ackSnapshot = transitionReadStateAckSnapshot(ignoredSnapshot, { type: 'readStateAck.updated', - input: input({isManualAck: false}), + input: input({ackedManually: false}), }); expect(selectReadStateAckDecision(ackSnapshot)).toMatchObject({ diff --git a/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.ts b/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.ts index d9a13298a..d23f7e985 100644 --- a/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.ts +++ b/fluxer_app/src/features/read_state/state/read_states/ReadStateAckMachine.ts @@ -7,9 +7,9 @@ export interface ReadStateAckInput { requestedMessageId?: string | null; lastMessageId: string | null; ackMessageId: string | null; - isManualAck: boolean; - loadedMessages: boolean; - canTrackUnreads: boolean; + ackedManually: boolean; + messagesLoaded: boolean; + supportsUnreadTracking: boolean; hasMentions: boolean; hasOldestUnreadMessage: boolean; hasStickyUnreadMessage: boolean; @@ -80,9 +80,9 @@ export const readStateAckMachine = setup({ }), }, guards: { - isManualAckHeld: ({context}) => !isOverrideAck(context) && context.isManualAck, - isNotLoaded: ({context}) => !isOverrideAck(context) && !context.loadedMessages, - isUntracked: ({context}) => !isOverrideAck(context) && !context.canTrackUnreads, + isManualAckHeld: ({context}) => !isOverrideAck(context) && context.ackedManually, + isNotLoaded: ({context}) => !isOverrideAck(context) && !context.messagesLoaded, + isUntracked: ({context}) => !isOverrideAck(context) && !context.supportsUnreadTracking, isMissingMessage: ({context}) => getFinalMessageId(context) == null, isOlderThanCurrentAck: ({context}) => { const finalMessageId = getFinalMessageId(context); diff --git a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntry.ts b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntry.ts index 5f76e2156..200887608 100644 --- a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntry.ts +++ b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntry.ts @@ -15,23 +15,23 @@ import type {Message as WireMessage} from '@fluxer/schema/src/domains/message/Me export class ReadStateEntry { readonly channelId: string; - _guildId: string | null = null; - loadedMessages = false; + storedGuildId: string | null = null; + messagesLoaded = false; readStateKnown = false; - private _lastMessageId: string | null = null; - private _lastMessageTimestamp = 0; - private _ackMessageId: string | null = null; - private _ackMessageTimestamp = 0; - ackPinTimestamp = 0; + private storedLastMessageId: string | null = null; + private storedLastMessageTimestamp = 0; + private storedAckMessageId: string | null = null; + private storedAckMessageTimestamp = 0; + acknowledgedPinTimestamp = 0; lastPinTimestamp = 0; - isManualAck = false; - private _oldestUnreadMessageId: string | null = null; - oldestUnreadMessageIdStale = false; - private _stickyUnreadMessageId: string | null = null; + ackedManually = false; + private storedOldestUnreadMessageId: string | null = null; + oldestUnreadNeedsRecompute = false; + private storedStickyUnreadMessageId: string | null = null; estimated = false; - private _unreadCount = 0; - private _mentionCount = 0; - outgoingAck: string | null = null; + private storedUnreadCount = 0; + private storedMentionCount = 0; + inFlightAckMessageId: string | null = null; serverVersion: string | null = null; snapshot?: { unread: boolean; @@ -47,81 +47,81 @@ export class ReadStateEntry { get guildId(): string | null { const channel = Channels.getChannel(this.channelId); - return channel?.guildId ?? this._guildId ?? null; + return channel?.guildId ?? this.storedGuildId ?? null; } get lastMessageId(): string | null { - return this._lastMessageId; + return this.storedLastMessageId; } set lastMessageId(messageId: string | null) { - this._lastMessageId = messageId; - this._lastMessageTimestamp = snowflakeTimestamp(messageId); + this.storedLastMessageId = messageId; + this.storedLastMessageTimestamp = snowflakeTimestamp(messageId); } get lastMessageTimestamp(): number { - return this._lastMessageTimestamp; + return this.storedLastMessageTimestamp; } get ackMessageId(): string | null { - return this._ackMessageId; + return this.storedAckMessageId; } set ackMessageId(messageId: string | null) { - this._ackMessageId = messageId; - this._ackMessageTimestamp = snowflakeTimestamp(messageId); + this.storedAckMessageId = messageId; + this.storedAckMessageTimestamp = snowflakeTimestamp(messageId); } get oldestUnreadMessageId(): string | null { - return this._oldestUnreadMessageId; + return this.storedOldestUnreadMessageId; } set oldestUnreadMessageId(messageId: string | null) { - this._oldestUnreadMessageId = messageId; - this.oldestUnreadMessageIdStale = false; + this.storedOldestUnreadMessageId = messageId; + this.oldestUnreadNeedsRecompute = false; } get stickyUnreadMessageId(): string | null { - return this._stickyUnreadMessageId; + return this.storedStickyUnreadMessageId; } set stickyUnreadMessageId(messageId: string | null) { - this._stickyUnreadMessageId = messageId; + this.storedStickyUnreadMessageId = messageId; } get visualUnreadMessageId(): string | null { - return this._stickyUnreadMessageId ?? this._oldestUnreadMessageId; + return this.storedStickyUnreadMessageId ?? this.storedOldestUnreadMessageId; } clearStickyUnread(): void { - this._stickyUnreadMessageId = null; + this.storedStickyUnreadMessageId = null; } get unreadCount(): number { - return this._unreadCount; + return this.storedUnreadCount; } set unreadCount(count: number) { - this._unreadCount = normalizeCount(count); + this.storedUnreadCount = normalizeCount(count); } get mentionCount(): number { - return this._mentionCount; + return this.storedMentionCount; } set mentionCount(count: number) { - this._mentionCount = normalizeCount(count); + this.storedMentionCount = normalizeCount(count); } - get oldestUnreadTimestamp(): number { + get oldestUnreadMessageTimestamp(): number { return snowflakeTimestamp(this.oldestUnreadMessageId); } get ackTimestamp(): number { - if (Number.isNaN(this._ackMessageTimestamp)) { + if (Number.isNaN(this.storedAckMessageTimestamp)) { return 0; } - return this._ackMessageTimestamp; + return this.storedAckMessageTimestamp; } get isPrivate(): boolean { @@ -129,17 +129,17 @@ export class ReadStateEntry { return channel?.isPrivate() ?? false; } - canTrackUnreads(): boolean { - return Channels.getChannel(this.channelId) != null || this._guildId != null; + supportsUnreadTracking(): boolean { + return Channels.getChannel(this.channelId) != null || this.storedGuildId != null; } private get statusModel() { return resolveReadStateEntryStatus({ - canTrackUnreads: this.canTrackUnreads(), + supportsUnreadTracking: this.supportsUnreadTracking(), hasBlockedDirectMessageRecipient: this.hasBlockedDirectMessageRecipient(), readStateKnown: this.readStateKnown, - lastMessageId: this._lastMessageId, - ackMessageId: this._ackMessageId, + lastMessageId: this.storedLastMessageId, + ackMessageId: this.storedAckMessageId, mentionCount: this.mentionCount, }); } @@ -148,8 +148,8 @@ export class ReadStateEntry { return this.statusModel.canBeUnread; } - canHaveMentions(): boolean { - return this.statusModel.canHaveMentions; + supportsMentions(): boolean { + return this.statusModel.supportsMentions; } hasUnread(): boolean { @@ -169,11 +169,11 @@ export class ReadStateEntry { return this.mentionCount > 0; } - hasUnreadOrMentions(): boolean { - return this.statusModel.hasUnreadOrMentions; + isUnreadOrMentioned(): boolean { + return this.statusModel.isUnreadOrMentioned; } - getGuildChannelUnreadState( + computeGuildChannelBadge( channel: { isPrivate(): boolean; guildId?: string; @@ -185,10 +185,10 @@ export class ReadStateEntry { mentionCount: number; unread: boolean; } { - if (!channel.isPrivate() && !this.canTrackUnreads()) { + if (!channel.isPrivate() && !this.supportsUnreadTracking()) { return {mentionCount: 0, unread: false}; } - const mentionCount = this.canHaveMentions() ? this.mentionCount : 0; + const mentionCount = this.supportsMentions() ? this.mentionCount : 0; if (isChannelMuted || isGuildMuted) { return {mentionCount, unread: false}; } @@ -206,12 +206,12 @@ export class ReadStateEntry { recomputeMentions?: boolean; } = {}, ): void { - const previousUnreadCount = this._unreadCount; + const previousUnreadCount = this.storedUnreadCount; if (ackMessageId !== undefined) { this.ackMessageId = ackMessageId; this.readStateKnown = true; } else { - this.ackMessageId = this._ackMessageId; + this.ackMessageId = this.storedAckMessageId; } this.oldestUnreadMessageId = null; this.estimated = false; @@ -242,11 +242,11 @@ export class ReadStateEntry { let loadedUnreadCount = 0; messages.forAll((message) => { if (!foundAckMessage) { - foundAckMessage = message.id === this._ackMessageId; - } else if (this._oldestUnreadMessageId == null) { - this._oldestUnreadMessageId = message.id; + foundAckMessage = message.id === this.storedAckMessageId; + } else if (this.storedOldestUnreadMessageId == null) { + this.storedOldestUnreadMessageId = message.id; } - if (compareMessageIds(message.id, this._ackMessageId) > 0) { + if (compareMessageIds(message.id, this.storedAckMessageId) > 0) { loadedUnreadCount++; if (recomputeMentions && !Relationships.isBlocked(message.author.id)) { const mentions = message.mentions; @@ -280,7 +280,7 @@ export class ReadStateEntry { } else { this.unreadCount = loadedUnreadCount; } - this.oldestUnreadMessageId = hasUnreadBoundary ? (this._oldestUnreadMessageId ?? oldestUnread) : null; + this.oldestUnreadMessageId = hasUnreadBoundary ? (this.storedOldestUnreadMessageId ?? oldestUnread) : null; } shouldMentionFor(message: MessageModel | WireMessage, userId: string, isPrivate: boolean): boolean { diff --git a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.bench.ts b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.bench.ts index d1935bffd..0182870d5 100644 --- a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.bench.ts +++ b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.bench.ts @@ -12,7 +12,7 @@ const INPUTS = Object.freeze( switch (index % 6) { case 0: return { - canTrackUnreads: false, + supportsUnreadTracking: false, hasBlockedDirectMessageRecipient: false, readStateKnown: true, lastMessageId: LAST_ID, @@ -21,7 +21,7 @@ const INPUTS = Object.freeze( }; case 1: return { - canTrackUnreads: true, + supportsUnreadTracking: true, hasBlockedDirectMessageRecipient: true, readStateKnown: true, lastMessageId: LAST_ID, @@ -30,7 +30,7 @@ const INPUTS = Object.freeze( }; case 2: return { - canTrackUnreads: true, + supportsUnreadTracking: true, hasBlockedDirectMessageRecipient: false, readStateKnown: false, lastMessageId: LAST_ID, @@ -39,7 +39,7 @@ const INPUTS = Object.freeze( }; case 3: return { - canTrackUnreads: true, + supportsUnreadTracking: true, hasBlockedDirectMessageRecipient: false, readStateKnown: true, lastMessageId: null, @@ -48,7 +48,7 @@ const INPUTS = Object.freeze( }; case 4: return { - canTrackUnreads: true, + supportsUnreadTracking: true, hasBlockedDirectMessageRecipient: false, readStateKnown: true, lastMessageId: LAST_ID, @@ -57,7 +57,7 @@ const INPUTS = Object.freeze( }; default: return { - canTrackUnreads: true, + supportsUnreadTracking: true, hasBlockedDirectMessageRecipient: false, readStateKnown: true, lastMessageId: LAST_ID, @@ -72,7 +72,7 @@ describe('ReadStateEntryStatusMachine benchmarks', () => { bench('resolves 100k mixed read-state entry statuses', () => { let unreadOrMentionCount = 0; for (const input of INPUTS) { - if (resolveReadStateEntryStatus(input).hasUnreadOrMentions) { + if (resolveReadStateEntryStatus(input).isUnreadOrMentioned) { unreadOrMentionCount++; } } diff --git a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.test.ts b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.test.ts index 0f817f284..5577484b3 100644 --- a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.test.ts +++ b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.test.ts @@ -17,7 +17,7 @@ const LAST_ID = fromTimestamp(BASE_TIMESTAMP + 2000); function input(overrides: Partial = {}): ReadStateEntryStatusInput { return { - canTrackUnreads: true, + supportsUnreadTracking: true, hasBlockedDirectMessageRecipient: false, readStateKnown: true, lastMessageId: LAST_ID, @@ -33,7 +33,7 @@ function expectResolvedState(overrides: Partial, expe describe('readStateEntryStatusMachine', () => { it('routes the read-state status by priority', () => { - expectResolvedState({canTrackUnreads: false, mentionCount: 1, ackMessageId: ACK_ID}, 'untracked'); + expectResolvedState({supportsUnreadTracking: false, mentionCount: 1, ackMessageId: ACK_ID}, 'untracked'); expectResolvedState({hasBlockedDirectMessageRecipient: true, mentionCount: 1, ackMessageId: ACK_ID}, 'blocked'); expectResolvedState({readStateKnown: false, ackMessageId: ACK_ID}, 'unknown'); expectResolvedState({lastMessageId: null, ackMessageId: ACK_ID}, 'unknown'); @@ -45,28 +45,28 @@ describe('readStateEntryStatusMachine', () => { expect(resolveReadStateEntryStatus(input({ackMessageId: ACK_ID, mentionCount: 2}))).toMatchObject({ state: 'unread', canBeUnread: true, - canHaveMentions: true, + supportsMentions: true, hasUnread: true, hasMentions: true, - hasUnreadOrMentions: true, + isUnreadOrMentioned: true, }); expect(resolveReadStateEntryStatus(input({hasBlockedDirectMessageRecipient: true, mentionCount: 2}))).toMatchObject( { state: 'blocked', canBeUnread: true, - canHaveMentions: false, + supportsMentions: false, hasUnread: false, hasMentions: true, - hasUnreadOrMentions: false, + isUnreadOrMentioned: false, }, ); - expect(resolveReadStateEntryStatus(input({canTrackUnreads: false, mentionCount: 2}))).toMatchObject({ + expect(resolveReadStateEntryStatus(input({supportsUnreadTracking: false, mentionCount: 2}))).toMatchObject({ state: 'untracked', canBeUnread: false, - canHaveMentions: false, + supportsMentions: false, hasUnread: false, hasMentions: true, - hasUnreadOrMentions: false, + isUnreadOrMentioned: false, }); }); @@ -81,7 +81,7 @@ describe('readStateEntryStatusMachine', () => { expect(selectReadStateEntryStatusModel(unreadSnapshot)).toMatchObject({ state: 'unread', - hasUnreadOrMentions: true, + isUnreadOrMentioned: true, }); }); }); diff --git a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.ts b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.ts index ce1f3bd3c..cdd7b3f54 100644 --- a/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.ts +++ b/fluxer_app/src/features/read_state/state/read_states/ReadStateEntryStatusMachine.ts @@ -4,7 +4,7 @@ import {assign, getInitialSnapshot, type SnapshotFrom, setup, transition} from ' import {compareMessageIds} from './shared'; export interface ReadStateEntryStatusInput { - canTrackUnreads: boolean; + supportsUnreadTracking: boolean; hasBlockedDirectMessageRecipient: boolean; readStateKnown: boolean; lastMessageId: string | null; @@ -22,10 +22,10 @@ export type ReadStateEntryStatusValue = 'untracked' | 'blocked' | 'unknown' | 'r export interface ReadStateEntryStatusModel { state: ReadStateEntryStatusValue; canBeUnread: boolean; - canHaveMentions: boolean; + supportsMentions: boolean; hasUnread: boolean; hasMentions: boolean; - hasUnreadOrMentions: boolean; + isUnreadOrMentioned: boolean; } function getStatusValue(snapshot: ReadStateEntryStatusSnapshot): ReadStateEntryStatusValue { @@ -49,7 +49,7 @@ function isUnread(context: ReadStateEntryStatusInput): boolean { } function getStatusValueFromInput(input: ReadStateEntryStatusInput): ReadStateEntryStatusValue { - if (!input.canTrackUnreads) return 'untracked'; + if (!input.supportsUnreadTracking) return 'untracked'; if (input.hasBlockedDirectMessageRecipient) return 'blocked'; if (!input.readStateKnown || input.lastMessageId == null) return 'unknown'; if (isUnread(input)) return 'unread'; @@ -62,15 +62,15 @@ function buildStatusModel( ): ReadStateEntryStatusModel { const hasMentions = input.mentionCount > 0; const canBeUnread = state !== 'untracked'; - const canHaveMentions = hasMentions && state !== 'untracked' && state !== 'blocked'; + const supportsMentions = hasMentions && state !== 'untracked' && state !== 'blocked'; const hasUnread = state === 'unread'; return { state, canBeUnread, - canHaveMentions, + supportsMentions, hasUnread, hasMentions, - hasUnreadOrMentions: hasUnread || canHaveMentions, + isUnreadOrMentioned: hasUnread || supportsMentions, }; } @@ -87,7 +87,7 @@ export const readStateEntryStatusMachine = setup({ }), }, guards: { - isUntracked: ({context}) => !context.canTrackUnreads, + isUntracked: ({context}) => !context.supportsUnreadTracking, isBlocked: ({context}) => context.hasBlockedDirectMessageRecipient, isUnknown: ({context}) => !context.readStateKnown || context.lastMessageId == null, isUnread: ({context}) => isUnread(context), diff --git a/fluxer_app/src/features/read_state/state/read_states/ReadStateServerAckMachine.ts b/fluxer_app/src/features/read_state/state/read_states/ReadStateServerAckMachine.ts index 81fd30891..a0cdafbde 100644 --- a/fluxer_app/src/features/read_state/state/read_states/ReadStateServerAckMachine.ts +++ b/fluxer_app/src/features/read_state/state/read_states/ReadStateServerAckMachine.ts @@ -68,7 +68,7 @@ export const readStateServerAckMachine = setup({ }, guards: { hasStaleVersion: ({context}) => hasStaleVersion(context), - isManualAck: ({context}) => context.manual, + ackedManually: ({context}) => context.manual, isOlderThanCurrentAck: ({context}) => isOlderThanCurrentAck(context), isCurrentAck: ({context}) => isCurrentAck(context), }, @@ -80,7 +80,7 @@ export const readStateServerAckMachine = setup({ routing: { always: [ {guard: 'hasStaleVersion', target: 'staleVersion'}, - {guard: 'isManualAck', target: 'manualAck'}, + {guard: 'ackedManually', target: 'manualAck'}, {guard: 'isOlderThanCurrentAck', target: 'olderMessage'}, {guard: 'isCurrentAck', target: 'currentAck'}, {target: 'newerAck'}, diff --git a/fluxer_app/src/features/read_state/state/read_states/shared.ts b/fluxer_app/src/features/read_state/state/read_states/shared.ts index c964863d8..4b09a5ab4 100644 --- a/fluxer_app/src/features/read_state/state/read_states/shared.ts +++ b/fluxer_app/src/features/read_state/state/read_states/shared.ts @@ -64,7 +64,7 @@ export interface PendingAck { export interface ArchivedReadState { ackMessageId: string | null; - ackPinTimestamp: number; + acknowledgedPinTimestamp: number; readStateKnown: boolean; } diff --git a/fluxer_app/src/features/search/state/QuickSwitcherResultGenerators.ts b/fluxer_app/src/features/search/state/QuickSwitcherResultGenerators.ts index d54328327..d2ee2dce1 100644 --- a/fluxer_app/src/features/search/state/QuickSwitcherResultGenerators.ts +++ b/fluxer_app/src/features/search/state/QuickSwitcherResultGenerators.ts @@ -123,7 +123,7 @@ function generateUnreadResults( if (excludedIds.has(channelId) || additionalExcludedChannelIds.has(channelId)) { return false; } - return ReadStates.hasUnreadOrMentions(channelId); + return ReadStates.isUnreadOrMentioned(channelId); }) .map((channelId) => Channels.getChannel(channelId)) .filter((channel): channel is Channel => channel != null) diff --git a/fluxer_app/src/features/ui/action_menu/FluxerButtonContextMenu.tsx b/fluxer_app/src/features/ui/action_menu/FluxerButtonContextMenu.tsx index 3cfdde21f..364dd9578 100644 --- a/fluxer_app/src/features/ui/action_menu/FluxerButtonContextMenu.tsx +++ b/fluxer_app/src/features/ui/action_menu/FluxerButtonContextMenu.tsx @@ -59,7 +59,7 @@ export const FluxerButtonContextMenu: React.FC = o const unreadGuildChannelIds: Array = []; for (const guild of guilds) { for (const channel of Channels.getGuildChannels(guild.id)) { - if (ReadStates.hasUnreadOrMentions(channel.id)) { + if (ReadStates.isUnreadOrMentioned(channel.id)) { unreadGuildChannelIds.push(channel.id); } }