mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): open channels at the unread boundary when the backlog exceeds one page (#1698)
This commit is contained in:
1 parent
3ff7189566
commit
97c29bf1fb
5 files changed
+113
-1
No files matched your search
@@ -11,6 +11,7 @@ import {ChannelMessages} from '@app/features/messaging/state/ChannelMessages';
|
||||
import type {ReactionEmoji} from '@app/features/messaging/utils/ReactionUtils';
|
||||
import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
|
||||
import SelectedGuild from '@app/features/navigation/state/SelectedGuild';
|
||||
import type {UnreadJumpAnchor} from '@app/features/read_state/state/ReadStates';
|
||||
import Relationships from '@app/features/relationship/state/Relationships';
|
||||
import Dimension from '@app/features/ui/state/Dimension';
|
||||
import Users from '@app/features/user/state/Users';
|
||||
@@ -347,7 +348,12 @@ class Messages {
|
||||
}
|
||||
|
||||
@action
|
||||
handleChannelSelect(action: {guildId?: string; channelId?: string | null; messageId?: string}): boolean {
|
||||
handleChannelSelect(action: {
|
||||
guildId?: string;
|
||||
channelId?: string | null;
|
||||
messageId?: string;
|
||||
unreadAnchor?: UnreadJumpAnchor | null;
|
||||
}): boolean {
|
||||
const channelId = action.channelId ?? action.guildId;
|
||||
if (channelId == null || channelId === ME) {
|
||||
return false;
|
||||
@@ -390,6 +396,17 @@ class Messages {
|
||||
}
|
||||
this.commitMessages(messages.mutate({loadingMore: true}));
|
||||
this.notifyChange();
|
||||
const unreadAnchor = action.unreadAnchor ?? null;
|
||||
if (unreadAnchor != null) {
|
||||
MessageCommands.jumpToMessage({
|
||||
channelId,
|
||||
messageId: unreadAnchor.messageId,
|
||||
offset: unreadAnchor.offset,
|
||||
flash: false,
|
||||
jumpType: JumpTypes.INSTANT,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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';
|
||||
|
||||
@@ -40,6 +41,7 @@ class NavigationSideEffects {
|
||||
guildId: guildId ?? undefined,
|
||||
channelId,
|
||||
messageId: messageId ?? undefined,
|
||||
unreadAnchor: ReadStates.getUnreadJumpAnchor(channelId),
|
||||
});
|
||||
Notification.handleChannelSelect({channelId});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import {resolveReadStateAckDecision} from '@app/features/read_state/state/read_s
|
||||
import {ReadStateEntry} from '@app/features/read_state/state/read_states/ReadStateEntry';
|
||||
import {resolveReadStateIncomingMessageDecision} from '@app/features/read_state/state/read_states/ReadStateIncomingMessageMachine';
|
||||
import {resolveReadStateServerAckDecision} from '@app/features/read_state/state/read_states/ReadStateServerAckMachine';
|
||||
import {
|
||||
resolveUnreadJumpAnchor,
|
||||
type UnreadJumpAnchor,
|
||||
} from '@app/features/read_state/state/read_states/ReadStateUnreadAnchor';
|
||||
import {
|
||||
ACK_BATCH_DELAY_MS,
|
||||
ACK_BATCH_SIZE,
|
||||
@@ -48,6 +52,8 @@ export type {GatewayReadState};
|
||||
|
||||
const logger = new Logger('ReadStates');
|
||||
|
||||
export type {UnreadJumpAnchor};
|
||||
|
||||
class ReadStates {
|
||||
private readonly states = new Map<ChannelId, ReadStateEntry>();
|
||||
private readonly archivedStates = new Map<ChannelId, ArchivedReadState>();
|
||||
@@ -345,6 +351,20 @@ class ReadStates {
|
||||
return state?.canTrackUnreads() ? state.visualUnreadMessageId : null;
|
||||
}
|
||||
|
||||
getUnreadJumpAnchor(channelId: string): UnreadJumpAnchor | null {
|
||||
const state = this.getIfExists(channelId);
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
return resolveUnreadJumpAnchor({
|
||||
canBeUnread: state.canBeUnread(),
|
||||
canTrackUnreads: state.canTrackUnreads(),
|
||||
hasUnread: state.hasUnread(),
|
||||
oldestUnreadMessageId: state.oldestUnreadMessageId,
|
||||
ackMessageId: state.ackMessageId,
|
||||
});
|
||||
}
|
||||
|
||||
getChannelIds(): Array<ChannelId> {
|
||||
return Array.from(this.states.keys());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {describe, expect, it} from 'vitest';
|
||||
import {resolveUnreadJumpAnchor, type UnreadJumpAnchorInput} from './ReadStateUnreadAnchor';
|
||||
|
||||
function input(overrides: Partial<UnreadJumpAnchorInput> = {}): UnreadJumpAnchorInput {
|
||||
return {
|
||||
canBeUnread: true,
|
||||
canTrackUnreads: true,
|
||||
hasUnread: true,
|
||||
oldestUnreadMessageId: '900',
|
||||
ackMessageId: '100',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('resolveUnreadJumpAnchor', () => {
|
||||
it('anchors on the oldest unread message when it is known', () => {
|
||||
expect(resolveUnreadJumpAnchor(input())).toEqual({messageId: '900', offset: 0});
|
||||
});
|
||||
|
||||
it('anchors just after the ack when the oldest unread message is unknown', () => {
|
||||
expect(resolveUnreadJumpAnchor(input({oldestUnreadMessageId: null}))).toEqual({messageId: '100', offset: 1});
|
||||
});
|
||||
|
||||
it('returns no anchor when the channel has nothing unread', () => {
|
||||
expect(resolveUnreadJumpAnchor(input({hasUnread: false}))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns no anchor when the channel cannot be unread', () => {
|
||||
expect(resolveUnreadJumpAnchor(input({canBeUnread: false}))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns no anchor when unreads are not trackable for the channel', () => {
|
||||
expect(resolveUnreadJumpAnchor(input({canTrackUnreads: false}))).toBeNull();
|
||||
});
|
||||
|
||||
it('returns no anchor when neither an unread boundary nor an ack exists', () => {
|
||||
expect(resolveUnreadJumpAnchor(input({oldestUnreadMessageId: null, ackMessageId: null}))).toBeNull();
|
||||
});
|
||||
|
||||
it('anchors on the unread boundary regardless of how far behind the ack is', () => {
|
||||
const anchor = resolveUnreadJumpAnchor(input({ackMessageId: '1', oldestUnreadMessageId: '2'}));
|
||||
expect(anchor).toEqual({messageId: '2', offset: 0});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export interface UnreadJumpAnchorInput {
|
||||
canBeUnread: boolean;
|
||||
canTrackUnreads: boolean;
|
||||
hasUnread: boolean;
|
||||
oldestUnreadMessageId: string | null;
|
||||
ackMessageId: string | null;
|
||||
}
|
||||
|
||||
export interface UnreadJumpAnchor {
|
||||
messageId: string;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
export function resolveUnreadJumpAnchor(input: UnreadJumpAnchorInput): UnreadJumpAnchor | null {
|
||||
if (!input.canBeUnread || !input.canTrackUnreads || !input.hasUnread) {
|
||||
return null;
|
||||
}
|
||||
if (input.oldestUnreadMessageId != null) {
|
||||
return {messageId: input.oldestUnreadMessageId, offset: 0};
|
||||
}
|
||||
if (input.ackMessageId == null) {
|
||||
return null;
|
||||
}
|
||||
return {messageId: input.ackMessageId, offset: 1};
|
||||
}
|
||||
Reference in new issue
Block a user