mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
refactor(app): drop the unread jump anchor now the scroller finds it (#1858)
This commit is contained in:
1 parent
af7469fa1f
commit
52781cfa2d
17 files changed
+148
-152
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<number | null>(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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<ChannelId, ReadStateEntry>();
|
||||
private readonly archivedStates = new Map<ChannelId, ArchivedReadState>();
|
||||
@@ -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<ChannelId> = [];
|
||||
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<WireMessage>}): 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ function input(overrides: Partial<ReadStateAckInput> = {}): 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({
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+7
-7
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -17,7 +17,7 @@ const LAST_ID = fromTimestamp(BASE_TIMESTAMP + 2000);
|
||||
|
||||
function input(overrides: Partial<ReadStateEntryStatusInput> = {}): ReadStateEntryStatusInput {
|
||||
return {
|
||||
canTrackUnreads: true,
|
||||
supportsUnreadTracking: true,
|
||||
hasBlockedDirectMessageRecipient: false,
|
||||
readStateKnown: true,
|
||||
lastMessageId: LAST_ID,
|
||||
@@ -33,7 +33,7 @@ function expectResolvedState(overrides: Partial<ReadStateEntryStatusInput>, 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
|
||||
@@ -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'},
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface PendingAck {
|
||||
|
||||
export interface ArchivedReadState {
|
||||
ackMessageId: string | null;
|
||||
ackPinTimestamp: number;
|
||||
acknowledgedPinTimestamp: number;
|
||||
readStateKnown: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -59,7 +59,7 @@ export const FluxerButtonContextMenu: React.FC<FluxerButtonContextMenuProps> = o
|
||||
const unreadGuildChannelIds: Array<string> = [];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user