fix(app): keep unsent messages and stop the unread jump on reconnect (#1861)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:33:17 +02:00
1 parent 2af0405317
commit b7f75e25ad
16 files changed
+295 -312

No files matched your search

@@ -445,7 +445,7 @@ const UnreadChannelCard = observer(function UnreadChannelCard({
highlightedMessageId: null, highlightedMessageId: null,
messageDisplayCompact, messageDisplayCompact,
messageGroupSpacing, messageGroupSpacing,
revealedMessageId: null, unblurredMessageId: null,
onMessageEdit: undefined, onMessageEdit: undefined,
onReveal: undefined, onReveal: undefined,
messageRowClassName: styles.messageRow, messageRowClassName: styles.messageRow,
@@ -34,7 +34,7 @@ interface RenderChannelStreamProps {
highlightedMessageId: string | null; highlightedMessageId: string | null;
messageDisplayCompact: boolean; messageDisplayCompact: boolean;
messageGroupSpacing: number; messageGroupSpacing: number;
revealedMessageId: string | null; unblurredMessageId: string | null;
onMessageEdit?: (target: HTMLElement) => void; onMessageEdit?: (target: HTMLElement) => void;
onReveal?: (messageId: string | null) => void; onReveal?: (messageId: string | null) => void;
messageRowClassName?: string; messageRowClassName?: string;
@@ -54,7 +54,7 @@ export function renderChannelStream(props: RenderChannelStreamProps): Array<Reac
highlightedMessageId, highlightedMessageId,
messageDisplayCompact, messageDisplayCompact,
messageGroupSpacing, messageGroupSpacing,
revealedMessageId, unblurredMessageId,
onMessageEdit, onMessageEdit,
onReveal, onReveal,
messageRowClassName, messageRowClassName,
@@ -182,14 +182,14 @@ export function renderChannelStream(props: RenderChannelStreamProps): Array<Reac
registerKey(item.key, 'BlockedMessageGroups', i, { registerKey(item.key, 'BlockedMessageGroups', i, {
groupId: item.key ?? null, groupId: item.key ?? null,
itemCount: Array.isArray(item.content) ? item.content.length : 0, itemCount: Array.isArray(item.content) ? item.content.length : 0,
revealed: item.key === revealedMessageId, revealed: item.key === unblurredMessageId,
variant, variant,
}); });
pushSpacerIfNeeded('regular', item.key ?? `${variant}-${i}`); pushSpacerIfNeeded('regular', item.key ?? `${variant}-${i}`);
nodes.push( nodes.push(
<BlockedMessageGroups <BlockedMessageGroups
key={item.key} key={item.key}
revealed={item.key === revealedMessageId} revealed={item.key === unblurredMessageId}
messageGroups={item.content as Array<ChannelStreamItem>} messageGroups={item.content as Array<ChannelStreamItem>}
onReveal={onReveal ?? (() => {})} onReveal={onReveal ?? (() => {})}
compact={messageDisplayCompact} compact={messageDisplayCompact}
@@ -94,7 +94,7 @@ interface MessagesStateSnapshot {
lastReadStateMessageId: string | null; lastReadStateMessageId: string | null;
messages: ChannelMessages; messages: ChannelMessages;
messageVersion: number; messageVersion: number;
revealedMessageId: string | null; unblurredMessageId: string | null;
permissionVersion: number; permissionVersion: number;
messageGroupSpacing: number; messageGroupSpacing: number;
fontSize: number; fontSize: number;
@@ -124,7 +124,7 @@ const readFromState = (channelId: string): MessagesStateSnapshot => {
lastReadStateMessageId: ReadStates.lastMessageId(channelId), lastReadStateMessageId: ReadStates.lastMessageId(channelId),
messages, messages,
messageVersion: messages.version, messageVersion: messages.version,
revealedMessageId: messages.revealedMessageId, unblurredMessageId: messages.unblurredMessageId,
permissionVersion: Permission.version, permissionVersion: Permission.version,
messageGroupSpacing: Accessibility.getMessageGroupSpacingValue(messageDisplayCompact), messageGroupSpacing: Accessibility.getMessageGroupSpacingValue(messageDisplayCompact),
fontSize: Accessibility.fontSize, fontSize: Accessibility.fontSize,
@@ -191,8 +191,8 @@ export const Messages = observer(function Messages({
const jumpHighlightTimeoutRef = useRef<number | null>(null); const jumpHighlightTimeoutRef = useRef<number | null>(null);
const lastJumpSequenceKeyRef = useRef<string | null>(null); const lastJumpSequenceKeyRef = useRef<string | null>(null);
const handleJumpHighlight = useCallback( const handleJumpHighlight = useCallback(
(targetMessageId: string | null, jumpSequenceId: number) => { (targetMessageId: string | null, jumpTicket: number) => {
const jumpSequenceKey = `${channel.id}:${jumpSequenceId}`; const jumpSequenceKey = `${channel.id}:${jumpTicket}`;
if (jumpSequenceKey === lastJumpSequenceKeyRef.current) return; if (jumpSequenceKey === lastJumpSequenceKeyRef.current) return;
lastJumpSequenceKeyRef.current = jumpSequenceKey; lastJumpSequenceKeyRef.current = jumpSequenceKey;
if (jumpHighlightTimeoutRef.current != null) { if (jumpHighlightTimeoutRef.current != null) {
@@ -290,7 +290,7 @@ export const Messages = observer(function Messages({
); );
const onScrollToPresent = useCallback(() => { const onScrollToPresent = useCallback(() => {
if (state.messages?.hasMoreAfter) { if (state.messages?.hasMoreAfter) {
MessageCommands.jumpToPresent(channel.id, MAX_MESSAGES_PER_CHANNEL); MessageCommands.jumpToLiveEdge(channel.id, MAX_MESSAGES_PER_CHANNEL);
} else { } else {
scrollManager.scrollSetToBottom(false); scrollManager.scrollSetToBottom(false);
} }
@@ -316,7 +316,7 @@ export const Messages = observer(function Messages({
}, [channel.id, state.ackMessageId, state.oldestUnreadMessageId]); }, [channel.id, state.ackMessageId, state.oldestUnreadMessageId]);
const onScrollToPresentAndAck = useCallback(() => { const onScrollToPresentAndAck = useCallback(() => {
if (state.messages?.hasMoreAfter) { if (state.messages?.hasMoreAfter) {
MessageCommands.jumpToPresent(channel.id, MAX_MESSAGES_PER_CHANNEL); MessageCommands.jumpToLiveEdge(channel.id, MAX_MESSAGES_PER_CHANNEL);
} else { } else {
scrollManager.scrollSetToBottom(false); scrollManager.scrollSetToBottom(false);
} }
@@ -357,7 +357,7 @@ export const Messages = observer(function Messages({
MessageEdit.subscribe(updateFromState), MessageEdit.subscribe(updateFromState),
]; ];
const onForceJumpToPresent = () => { const onForceJumpToPresent = () => {
MessageCommands.jumpToPresent(channel.id, MAX_MESSAGES_PER_CHANNEL); MessageCommands.jumpToLiveEdge(channel.id, MAX_MESSAGES_PER_CHANNEL);
}; };
const onScrollPageUp = () => scrollManager.scrollPageUp(true); const onScrollPageUp = () => scrollManager.scrollPageUp(true);
const onScrollPageDown = () => scrollManager.scrollPageDown(true); const onScrollPageDown = () => scrollManager.scrollPageDown(true);
@@ -523,16 +523,16 @@ export const Messages = observer(function Messages({
}, [channel.id, state.messages?.ready, state.messageVersion]); }, [channel.id, state.messages?.ready, state.messageVersion]);
const immediateHighlightedMessageId = (() => { const immediateHighlightedMessageId = (() => {
const messages = state.messages; const messages = state.messages;
if (!messages?.ready || !messages.jumped || !messages.jumpFlash || !messages.jumpTargetId) { if (!messages?.ready || !messages.hasJumped || !messages.jumpHighlight || !messages.jumpDestinationId) {
return null; return null;
} }
return messages.jumpTargetId !== channel.id ? messages.jumpTargetId : null; return messages.jumpDestinationId !== channel.id ? messages.jumpDestinationId : null;
})(); })();
const highlightedMessageId = immediateHighlightedMessageId ?? state.highlightedMessageId; const highlightedMessageId = immediateHighlightedMessageId ?? state.highlightedMessageId;
const collapsedMessageVisibility = useMemo( const collapsedMessageVisibility = useMemo(
() => ({ () => ({
isMessageRevealed: (message: Message) => { isMessageRevealed: (message: Message) => {
if (!state.revealedMessageId || !state.messages || message.channelId !== channel.id) { if (!state.unblurredMessageId || !state.messages || message.channelId !== channel.id) {
return false; return false;
} }
return ( return (
@@ -541,11 +541,11 @@ export const Messages = observer(function Messages({
messages: state.messages, messages: state.messages,
messageId: message.id, messageId: message.id,
treatSpam: true, treatSpam: true,
}) === state.revealedMessageId }) === state.unblurredMessageId
); );
}, },
}), }),
[channel, state.messages, state.revealedMessageId, state.messageVersion, spammerOverrideVersion], [channel, state.messages, state.unblurredMessageId, state.messageVersion, spammerOverrideVersion],
); );
const {canChat, canAttachFiles} = useMemo( const {canChat, canAttachFiles} = useMemo(
() => checkPermissions(channel), () => checkPermissions(channel),
@@ -560,7 +560,7 @@ export const Messages = observer(function Messages({
highlightedMessageId, highlightedMessageId,
messageDisplayCompact: state.messageDisplayCompact, messageDisplayCompact: state.messageDisplayCompact,
messageGroupSpacing: state.messageGroupSpacing, messageGroupSpacing: state.messageGroupSpacing,
revealedMessageId: state.revealedMessageId, unblurredMessageId: state.unblurredMessageId,
onMessageEdit, onMessageEdit,
onReveal, onReveal,
}); });
@@ -571,7 +571,7 @@ export const Messages = observer(function Messages({
highlightedMessageId, highlightedMessageId,
state.messageDisplayCompact, state.messageDisplayCompact,
state.messageGroupSpacing, state.messageGroupSpacing,
state.revealedMessageId, state.unblurredMessageId,
onMessageEdit, onMessageEdit,
onReveal, onReveal,
]); ]);
@@ -120,7 +120,7 @@ const ForwardedFromSource = observer(({message}: {message: Message}) => {
const handleJumpToOriginal = useCallback(() => { const handleJumpToOriginal = useCallback(() => {
if (message.messageReference && sourceChannel) { if (message.messageReference && sourceChannel) {
goToMessage(message.messageReference.channel_id, message.messageReference.message_id, { goToMessage(message.messageReference.channel_id, message.messageReference.message_id, {
returnTargetId: message.id, returnToMessageId: message.id,
returnChannelId: message.channelId, returnChannelId: message.channelId,
}); });
} }
@@ -23,7 +23,7 @@ export const PinSystemMessage = observer(({message}: PinSystemMessageProps) => {
const jumpToMessage = useCallback(() => { const jumpToMessage = useCallback(() => {
if (message.messageReference?.message_id) { if (message.messageReference?.message_id) {
goToMessage(message.channelId, message.messageReference.message_id, { goToMessage(message.channelId, message.messageReference.message_id, {
returnTargetId: message.id, returnToMessageId: message.id,
returnChannelId: message.channelId, returnChannelId: message.channelId,
}); });
} }
@@ -66,7 +66,7 @@ export const ReplyPreview = observer(
const jumpToRepliedMessage = useCallback(() => { const jumpToRepliedMessage = useCallback(() => {
if (message.messageReference?.message_id) { if (message.messageReference?.message_id) {
goToMessage(message.channelId, message.messageReference.message_id, { goToMessage(message.channelId, message.messageReference.message_id, {
returnTargetId: message.id, returnToMessageId: message.id,
returnChannelId: message.channelId, returnChannelId: message.channelId,
}); });
} }
@@ -185,5 +185,5 @@ function handleReadyInternal(data: ReadyPayload, context: GatewayHandlerContext)
MediaEngine.handleConnectionOpen(guilds); MediaEngine.handleConnectionOpen(guilds);
Initialization.setReady(); Initialization.setReady();
context.setReady(); context.setReady();
Messages.handleConnectionOpen((channelId) => ReadStates.getUnreadJumpAnchor(channelId)); Messages.handleConnectionOpen();
} }
@@ -100,7 +100,7 @@ export interface JumpToMessageOptions {
messageId: string; messageId: string;
flash?: boolean; flash?: boolean;
offset?: number; offset?: number;
returnTargetId?: string | null; returnToMessageId?: string | null;
returnChannelId?: string | null; returnChannelId?: string | null;
returnGuildId?: string | null; returnGuildId?: string | null;
jumpType?: JumpType; jumpType?: JumpType;
@@ -141,7 +141,7 @@ function makeFetchKey(
return ( return (
`${channelId}${SEP}${before ?? ''}${SEP}${after ?? ''}${SEP}${limit}${SEP}${throwOnError}${SEP}` + `${channelId}${SEP}${before ?? ''}${SEP}${after ?? ''}${SEP}${limit}${SEP}${throwOnError}${SEP}` +
`${jump.present ? '1' : '0'}${SEP}${jump.messageId ?? ''}${SEP}${jump.offset ?? 0}${SEP}` + `${jump.present ? '1' : '0'}${SEP}${jump.messageId ?? ''}${SEP}${jump.offset ?? 0}${SEP}` +
`${jump.flash ? '1' : '0'}${SEP}${jump.returnMessageId ?? ''}${SEP}` + `${jump.flash ? '1' : '0'}${SEP}${jump.returnToMessageId ?? ''}${SEP}` +
`${jump.returnChannelId ?? ''}${SEP}${jump.returnGuildId ?? ''}${SEP}${jump.jumpType ?? ''}` `${jump.returnChannelId ?? ''}${SEP}${jump.returnGuildId ?? ''}${SEP}${jump.jumpType ?? ''}`
); );
} }
@@ -291,14 +291,14 @@ interface SendMessageParams {
tts?: boolean; tts?: boolean;
} }
export function jumpToPresent(channelId: string, limit = MAX_MESSAGES_PER_CHANNEL): void { export function jumpToLiveEdge(channelId: string, limit = MAX_MESSAGES_PER_CHANNEL): void {
NavigationCommands.clearMessageIdForChannel(channelId); NavigationCommands.clearMessageIdForChannel(channelId);
logger.debug(`Jumping to present in channel ${channelId}`); logger.debug(`Jumping to present in channel ${channelId}`);
ReadStateCommands.clearStickyUnread(channelId); ReadStateCommands.clearStickyUnread(channelId);
const jump: JumpOptions = { const jump: JumpOptions = {
present: true, present: true,
}; };
if (Messages.hasPresent(channelId)) { if (Messages.hasNewestMessages(channelId)) {
Messages.handleLoadMessagesSuccessCached({channelId, jump, limit}); Messages.handleLoadMessagesSuccessCached({channelId, jump, limit});
} else { } else {
fetchMessages(channelId, null, null, limit, jump); fetchMessages(channelId, null, null, limit, jump);
@@ -310,7 +310,7 @@ export function jumpToMessage({
messageId, messageId,
flash = true, flash = true,
offset, offset,
returnTargetId, returnToMessageId,
returnChannelId, returnChannelId,
returnGuildId, returnGuildId,
jumpType, jumpType,
@@ -320,7 +320,7 @@ export function jumpToMessage({
messageId: messageId as MessageId, messageId: messageId as MessageId,
flash, flash,
offset, offset,
returnMessageId: returnTargetId as MessageId | null | undefined, returnToMessageId: returnToMessageId as MessageId | null | undefined,
returnChannelId, returnChannelId,
returnGuildId, returnGuildId,
jumpType, jumpType,
@@ -337,10 +337,10 @@ function getMessageFetchCacheHit(
if (jump?.messageId && messages.has(jump.messageId, false)) { if (jump?.messageId && messages.has(jump.messageId, false)) {
return 'jump'; return 'jump';
} }
if (before && messages.hasBeforeCached(before)) { if (before && messages.canServeOlderFrom(before)) {
return 'before'; return 'before';
} }
if (after && messages.hasAfterCached(after)) { if (after && messages.canServeNewerFrom(after)) {
return 'after'; return 'after';
} }
return null; return null;
@@ -208,7 +208,7 @@ interface JumpLinkMentionProps {
channel: Channel; channel: Channel;
guild: Guild | null; guild: Guild | null;
messageId?: string; messageId?: string;
returnTargetId?: string; returnToMessageId?: string;
returnChannelId?: string; returnChannelId?: string;
url: string; url: string;
i18n: I18n; i18n: I18n;
@@ -264,7 +264,7 @@ const JumpLinkMention = observer(function JumpLinkMention({
channel, channel,
guild, guild,
messageId, messageId,
returnTargetId, returnToMessageId,
returnChannelId, returnChannelId,
url, url,
i18n, i18n,
@@ -276,11 +276,11 @@ const JumpLinkMention = observer(function JumpLinkMention({
return; return;
} }
if (messageId) { if (messageId) {
goToMessage(channel.id, messageId, {returnTargetId, returnChannelId}); goToMessage(channel.id, messageId, {returnToMessageId, returnChannelId});
return; return;
} }
NavigationCommands.selectChannel(channel.guildId ?? undefined, channel.id); NavigationCommands.selectChannel(channel.guildId ?? undefined, channel.id);
}, [channel, messageId, returnChannelId, returnTargetId]); }, [channel, messageId, returnChannelId, returnToMessageId]);
const handleClick = useCallback( const handleClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => { (event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return; if (!interactive) return;
@@ -674,7 +674,7 @@ export const LinkRenderer = observer(function LinkRenderer({
channel={jumpChannel} channel={jumpChannel}
guild={jumpGuild} guild={jumpGuild}
messageId={messageJumpTarget?.messageId} messageId={messageJumpTarget?.messageId}
returnTargetId={options.messageId} returnToMessageId={options.messageId}
returnChannelId={options.channelId} returnChannelId={options.channelId}
url={url} url={url}
i18n={i18n} i18n={i18n}
@@ -828,7 +828,7 @@ export const LinkRenderer = observer(function LinkRenderer({
return; return;
} }
goToMessage(targetChannelId, targetMessageId, { goToMessage(targetChannelId, targetMessageId, {
returnTargetId: options.messageId, returnToMessageId: options.messageId,
returnChannelId: options.channelId, returnChannelId: options.channelId,
}); });
}; };
@@ -24,7 +24,7 @@ export interface JumpOptions {
offset?: number; offset?: number;
present?: boolean; present?: boolean;
flash?: boolean; flash?: boolean;
returnMessageId?: MessageId | null; returnToMessageId?: MessageId | null;
returnChannelId?: string | null; returnChannelId?: string | null;
returnGuildId?: string | null; returnGuildId?: string | null;
jumpType?: JumpType; jumpType?: JumpType;
@@ -34,14 +34,14 @@ interface JumpToMessageOptions {
messageId: string; messageId: string;
flash?: boolean; flash?: boolean;
offset?: number; offset?: number;
returnTargetId?: MessageId | null; returnToMessageId?: MessageId | null;
returnChannelId?: string | null; returnChannelId?: string | null;
returnGuildId?: string | null; returnGuildId?: string | null;
jumpType?: JumpType; jumpType?: JumpType;
} }
interface LoadCompleteOptions { interface LoadCompleteOptions {
newMessages: Array<MessageInput>; windowMessages: Array<MessageInput>;
isBefore?: boolean; isBefore?: boolean;
isAfter?: boolean; isAfter?: boolean;
jump?: JumpOptions | null; jump?: JumpOptions | null;
@@ -61,6 +61,10 @@ function isUploadPlaceholder(message: Message): boolean {
return UploadingAttachment.isInSendingMessage(message); return UploadingAttachment.isInSendingMessage(message);
} }
function unsentCorrelationKey(message: Message): string {
return `${message.author.id}\u0000${message.messageReference?.message_id ?? ''}\u0000${message.content}`;
}
function embedsEqualIgnoringId(a: unknown, b: unknown): boolean { function embedsEqualIgnoringId(a: unknown, b: unknown): boolean {
if (a === b) return true; if (a === b) return true;
if (a == null || b == null) return a === b; if (a == null || b == null) return a === b;
@@ -182,7 +186,7 @@ class MessageBufferSegment {
this.items = this.items.filter((m) => m.id !== id); this.items = this.items.filter((m) => m.id !== id);
} }
removeMany(ids: Array<string>): void { removeIds(ids: Array<string>): void {
if (!ids.length) return; if (!ids.length) return;
const idSet = ids.length > 1 ? new Set(ids) : null; const idSet = ids.length > 1 ? new Set(ids) : null;
for (const id of ids) { for (const id of ids) {
@@ -219,9 +223,7 @@ class MessageBufferSegment {
if (this.items.length === 0) { if (this.items.length === 0) {
this.reachedBoundary = boundaryAtInsert; this.reachedBoundary = boundaryAtInsert;
} }
const combinedSize = this.items.length + batch.length; if (this.items.length + batch.length > MAX_MESSAGE_CACHE_SIZE) {
const truncated = combinedSize > MAX_MESSAGE_CACHE_SIZE;
if (truncated) {
this.reachedBoundary = false; this.reachedBoundary = false;
if (batch.length >= MAX_MESSAGE_CACHE_SIZE) { if (batch.length >= MAX_MESSAGE_CACHE_SIZE) {
this.items = this.fromOlderSide this.items = this.fromOlderSide
@@ -229,27 +231,27 @@ class MessageBufferSegment {
: batch.slice(0, MAX_MESSAGE_CACHE_SIZE); : batch.slice(0, MAX_MESSAGE_CACHE_SIZE);
} else { } else {
const available = MAX_MESSAGE_CACHE_SIZE - batch.length; const available = MAX_MESSAGE_CACHE_SIZE - batch.length;
this.items = this.fromOlderSide const kept = this.fromOlderSide
? this.items.slice(Math.max(this.items.length - available, 0)) ? this.items.slice(Math.max(this.items.length - available, 0))
: this.items.slice(0, available); : this.items.slice(0, available);
this.items = this.fromOlderSide ? kept.concat(batch) : batch.concat(kept);
} }
}
if (truncated) {
this.items = this.fromOlderSide ? this.items.concat(batch) : batch.concat(this.items);
this.keyedById = {}; this.keyedById = {};
for (const msg of this.items) { for (const msg of this.items) {
this.keyedById[msg.id] = msg; this.keyedById[msg.id] = msg;
} }
} else if (this.fromOlderSide) { return;
}
if (this.fromOlderSide) {
for (const msg of batch) { for (const msg of batch) {
this.items.push(msg); this.items.push(msg);
this.keyedById[msg.id] = msg; this.keyedById[msg.id] = msg;
} }
} else { return;
this.items.unshift(...batch); }
for (const msg of batch) { this.items.unshift(...batch);
this.keyedById[msg.id] = msg; for (const msg of batch) {
} this.keyedById[msg.id] = msg;
} }
} }
@@ -287,19 +289,20 @@ export class ChannelMessages {
readonly channelId: string; readonly channelId: string;
ready = false; ready = false;
jumpType: JumpType = JumpTypes.ANIMATED; jumpType: JumpType = JumpTypes.ANIMATED;
jumpTargetId: string | null = null; jumpDestinationId: string | null = null;
jumpTargetOffset = 0; jumpDestinationOffset = 0;
jumpSequenceId = 1; jumpTicket = 1;
jumped = false; hasJumped = false;
jumpedToPresent = false; jumpedToPresent = false;
jumpFlash = true; landedAtLiveEdge = false;
jumpReturnTargetId: string | null = null; jumpHighlight = true;
jumpReturnMessageId: string | null = null;
jumpReturnChannelId: string | null = null; jumpReturnChannelId: string | null = null;
jumpReturnGuildId: string | null = null; jumpReturnGuildId: string | null = null;
hasMoreBefore = true; hasMoreBefore = true;
hasMoreAfter = false; hasMoreAfter = false;
loadingMore = false; loadingMore = false;
revealedMessageId: string | null = null; unblurredMessageId: string | null = null;
cached = false; cached = false;
error = false; error = false;
version = 0; version = 0;
@@ -318,8 +321,8 @@ export class ChannelMessages {
return ChannelMessages.channelCache.get(channelId); return ChannelMessages.channelCache.get(channelId);
} }
static hasPresent(channelId: string): boolean { static hasNewestMessages(channelId: string): boolean {
return ChannelMessages.get(channelId)?.hasPresent() ?? false; return ChannelMessages.get(channelId)?.hasNewestMessages() ?? false;
} }
static getOrCreate(channelId: string): ChannelMessages { static getOrCreate(channelId: string): ChannelMessages {
@@ -350,7 +353,7 @@ export class ChannelMessages {
ChannelMessages.retainedChannelIds.delete(channelId); ChannelMessages.retainedChannelIds.delete(channelId);
} }
static clearCache(channelId: string): void { static dropBuffers(channelId: string): void {
const instance = ChannelMessages.channelCache.get(channelId); const instance = ChannelMessages.channelCache.get(channelId);
if (!instance) return; if (!instance) return;
instance.beforeBuffer.clear(); instance.beforeBuffer.clear();
@@ -419,7 +422,7 @@ export class ChannelMessages {
this.afterBuffer = new MessageBufferSegment(false); this.afterBuffer = new MessageBufferSegment(false);
} }
mutate(patch: Partial<ChannelMessages>): ChannelMessages { withPatch(patch: Partial<ChannelMessages>): ChannelMessages {
return this.cloneAnd(patch); return this.cloneAnd(patch);
} }
@@ -451,13 +454,13 @@ export class ChannelMessages {
return this.messageList.reduce(reducer, initial); return this.messageList.reduce(reducer, initial);
} }
forAll(callback: (m: Message, idx: number, arr: Array<Message>) => void, thisArg?: unknown): void { forEachBuffered(callback: (m: Message, idx: number, arr: Array<Message>) => void, thisArg?: unknown): void {
this.beforeBuffer.forEach(callback, thisArg); this.beforeBuffer.forEach(callback, thisArg);
this.messageList.forEach(callback, thisArg); this.messageList.forEach(callback, thisArg);
this.afterBuffer.forEach(callback, thisArg); this.afterBuffer.forEach(callback, thisArg);
} }
findOldest(predicate: (m: Message) => boolean): Message | undefined { searchFromOldest(predicate: (m: Message) => boolean): Message | undefined {
return ( return (
this.beforeBuffer.messages.find(predicate) ?? this.beforeBuffer.messages.find(predicate) ??
this.messageList.find(predicate) ?? this.messageList.find(predicate) ??
@@ -465,7 +468,7 @@ export class ChannelMessages {
); );
} }
findNewest(predicate: (m: Message) => boolean): Message | undefined { searchFromNewest(predicate: (m: Message) => boolean): Message | undefined {
const after = this.afterBuffer.messages; const after = this.afterBuffer.messages;
for (let i = after.length - 1; i >= 0; i--) { for (let i = after.length - 1; i >= 0; i--) {
if (predicate(after[i])) return after[i]; if (predicate(after[i])) return after[i];
@@ -498,11 +501,11 @@ export class ChannelMessages {
return this.beforeBuffer.get(id) ?? this.afterBuffer.get(id); return this.beforeBuffer.get(id) ?? this.afterBuffer.get(id);
} }
getByIndex(index: number): Message | undefined { atIndex(index: number): Message | undefined {
return this.messageList[index]; return this.messageList[index];
} }
getAfter(id: string): Message | null { nextAfter(id: string): Message | null {
const current = this.get(id); const current = this.get(id);
if (!current) return null; if (!current) return null;
const idx = this.messageList.indexOf(current); const idx = this.messageList.indexOf(current);
@@ -520,11 +523,11 @@ export class ChannelMessages {
return this.messageList.findIndex((m) => m.id === id); return this.messageList.findIndex((m) => m.id === id);
} }
hasPresent(): boolean { hasNewestMessages(): boolean {
return (this.afterBuffer.size > 0 && this.afterBuffer.isBoundary) || !this.hasMoreAfter; return (this.afterBuffer.size > 0 && this.afterBuffer.isBoundary) || !this.hasMoreAfter;
} }
hasBeforeCached(beforeId: string): boolean { canServeOlderFrom(beforeId: string): boolean {
if (this.messageList.length === 0 || this.beforeBuffer.size === 0) { if (this.messageList.length === 0 || this.beforeBuffer.size === 0) {
return false; return false;
} }
@@ -532,7 +535,7 @@ export class ChannelMessages {
return Boolean(first && first.id === beforeId); return Boolean(first && first.id === beforeId);
} }
hasAfterCached(afterId: string): boolean { canServeNewerFrom(afterId: string): boolean {
if (this.messageList.length === 0 || this.afterBuffer.size === 0) { if (this.messageList.length === 0 || this.afterBuffer.size === 0) {
return false; return false;
} }
@@ -587,7 +590,7 @@ export class ChannelMessages {
}, true); }, true);
} }
removeMany(ids: Array<string>): ChannelMessages { removeIds(ids: Array<string>): ChannelMessages {
if (!ids.some((id) => this.has(id))) return this; if (!ids.some((id) => this.has(id))) return this;
const idSet = ids.length > 1 ? new Set(ids) : null; const idSet = ids.length > 1 ? new Set(ids) : null;
return this.cloneAnd((draft) => { return this.cloneAnd((draft) => {
@@ -598,8 +601,8 @@ export class ChannelMessages {
idSet !== null idSet !== null
? draft.messageList.filter((m) => !idSet.has(m.id)) ? draft.messageList.filter((m) => !idSet.has(m.id))
: draft.messageList.filter((m) => m.id !== ids[0]); : draft.messageList.filter((m) => m.id !== ids[0]);
draft.beforeBuffer.removeMany(ids); draft.beforeBuffer.removeIds(ids);
draft.afterBuffer.removeMany(ids); draft.afterBuffer.removeIds(ids);
}, true); }, true);
} }
@@ -651,7 +654,7 @@ export class ChannelMessages {
}, true); }, true);
} }
truncateTop(maxCount: number, deepCopy = true): ChannelMessages { trimOldest(maxCount: number, deepCopy = true): ChannelMessages {
const overflow = this.messageList.length - maxCount; const overflow = this.messageList.length - maxCount;
if (overflow <= 0) return this; if (overflow <= 0) return this;
return this.cloneAnd((draft) => { return this.cloneAnd((draft) => {
@@ -664,7 +667,7 @@ export class ChannelMessages {
}, deepCopy); }, deepCopy);
} }
truncateBottom(maxCount: number, deepCopy = true): ChannelMessages { trimNewest(maxCount: number, deepCopy = true): ChannelMessages {
if (this.messageList.length <= maxCount) return this; if (this.messageList.length <= maxCount) return this;
return this.cloneAnd((draft) => { return this.cloneAnd((draft) => {
for (let i = maxCount; i < this.messageList.length; i++) { for (let i = maxCount; i < this.messageList.length; i++) {
@@ -676,18 +679,18 @@ export class ChannelMessages {
}, deepCopy); }, deepCopy);
} }
truncate(trimBottom: boolean, trimTop: boolean): ChannelMessages { trimToWindow(trimBottom: boolean, trimTop: boolean): ChannelMessages {
if (this.length <= MAX_LOADED_MESSAGES) return this; if (this.length <= MAX_LOADED_MESSAGES) return this;
if (trimBottom) { if (trimBottom) {
return this.truncateBottom(TRUNCATED_MESSAGE_VIEW_SIZE); return this.trimNewest(TRUNCATED_MESSAGE_VIEW_SIZE);
} }
if (trimTop) { if (trimTop) {
return this.truncateTop(TRUNCATED_MESSAGE_VIEW_SIZE); return this.trimOldest(TRUNCATED_MESSAGE_VIEW_SIZE);
} }
return this; return this;
} }
jumpToPresent(limit: number): ChannelMessages { jumpToLiveEdge(limit: number): ChannelMessages {
return this.cloneAnd((draft) => { return this.cloneAnd((draft) => {
const allAfter = draft.afterBuffer.takeAll(); const allAfter = draft.afterBuffer.takeAll();
draft.hasMoreAfter = false; draft.hasMoreAfter = false;
@@ -699,15 +702,15 @@ export class ChannelMessages {
draft.clearAllMessages(); draft.clearAllMessages();
draft.mergeInto(visible); draft.mergeInto(visible);
draft.hasMoreBefore = draft.beforeBuffer.size > 0; draft.hasMoreBefore = draft.beforeBuffer.size > 0;
draft.jumped = true; draft.hasJumped = true;
draft.jumpTargetId = null; draft.jumpDestinationId = null;
draft.jumpTargetOffset = 0; draft.jumpDestinationOffset = 0;
draft.jumpedToPresent = true; draft.landedAtLiveEdge = true;
draft.jumpFlash = false; draft.jumpHighlight = false;
draft.jumpReturnTargetId = null; draft.jumpReturnMessageId = null;
draft.jumpReturnChannelId = null; draft.jumpReturnChannelId = null;
draft.jumpReturnGuildId = null; draft.jumpReturnGuildId = null;
draft.jumpSequenceId += 1; draft.jumpTicket += 1;
draft.ready = true; draft.ready = true;
draft.loadingMore = false; draft.loadingMore = false;
}, true); }, true);
@@ -717,22 +720,22 @@ export class ChannelMessages {
messageId, messageId,
flash = true, flash = true,
offset, offset,
returnTargetId, returnToMessageId,
returnChannelId, returnChannelId,
returnGuildId, returnGuildId,
jumpType, jumpType,
}: JumpToMessageOptions): ChannelMessages { }: JumpToMessageOptions): ChannelMessages {
return this.cloneAnd((draft) => { return this.cloneAnd((draft) => {
draft.jumped = true; draft.hasJumped = true;
draft.jumpedToPresent = false; draft.landedAtLiveEdge = false;
draft.jumpType = jumpType ?? JumpTypes.ANIMATED; draft.jumpType = jumpType ?? JumpTypes.ANIMATED;
draft.jumpTargetId = messageId; draft.jumpDestinationId = messageId;
draft.jumpTargetOffset = messageId && offset != null ? offset : 0; draft.jumpDestinationOffset = messageId && offset != null ? offset : 0;
draft.jumpSequenceId += 1; draft.jumpTicket += 1;
draft.jumpFlash = flash; draft.jumpHighlight = flash;
draft.jumpReturnTargetId = returnTargetId ?? null; draft.jumpReturnMessageId = returnToMessageId ?? null;
draft.jumpReturnChannelId = returnTargetId ? (returnChannelId ?? draft.channelId) : null; draft.jumpReturnChannelId = returnToMessageId ? (returnChannelId ?? draft.channelId) : null;
draft.jumpReturnGuildId = returnTargetId ? (returnGuildId ?? null) : null; draft.jumpReturnGuildId = returnToMessageId ? (returnGuildId ?? null) : null;
draft.ready = true; draft.ready = true;
draft.loadingMore = false; draft.loadingMore = false;
}, false); }, false);
@@ -740,20 +743,20 @@ export class ChannelMessages {
clearJumpTarget(options: {clearReturnTarget?: boolean} = {}): ChannelMessages { clearJumpTarget(options: {clearReturnTarget?: boolean} = {}): ChannelMessages {
const patch: Partial<ChannelMessages> = { const patch: Partial<ChannelMessages> = {
jumped: false, hasJumped: false,
jumpedToPresent: false, landedAtLiveEdge: false,
jumpTargetId: null, jumpDestinationId: null,
jumpTargetOffset: 0, jumpDestinationOffset: 0,
}; };
if (options.clearReturnTarget) { if (options.clearReturnTarget) {
patch.jumpReturnTargetId = null; patch.jumpReturnMessageId = null;
patch.jumpReturnChannelId = null; patch.jumpReturnChannelId = null;
patch.jumpReturnGuildId = null; patch.jumpReturnGuildId = null;
} }
return this.cloneAnd(patch); return this.cloneAnd(patch);
} }
loadFromCache(before: boolean, limit: number): ChannelMessages { drainBufferedSide(before: boolean, limit: number): ChannelMessages {
let next = this.cloneAnd((draft) => { let next = this.cloneAnd((draft) => {
const buffer = before ? draft.beforeBuffer : draft.afterBuffer; const buffer = before ? draft.beforeBuffer : draft.afterBuffer;
draft.mergeInto(buffer.take(limit), before); draft.mergeInto(buffer.take(limit), before);
@@ -764,14 +767,14 @@ export class ChannelMessages {
draft.loadingMore = false; draft.loadingMore = false;
}, true); }, true);
if (before) { if (before) {
next = next.truncate(true, false); next = next.trimToWindow(true, false);
} else { } else {
next = next.truncate(false, true); next = next.trimToWindow(false, true);
} }
return next; return next;
} }
receiveMessage(message: MessageInput, truncateFromTop = true): ChannelMessages { applyIncomingMessage(message: MessageInput, truncateFromTop = true): ChannelMessages {
const wire = toWireMessage(message); const wire = toWireMessage(message);
const possibleNonce = wire.nonce ?? null; const possibleNonce = wire.nonce ?? null;
const previous = possibleNonce ? this.get(possibleNonce, true) : null; const previous = possibleNonce ? this.get(possibleNonce, true) : null;
@@ -804,15 +807,15 @@ export class ChannelMessages {
private appendIncomingMessage(message: MessageInput, truncateFromTop: boolean): ChannelMessages { private appendIncomingMessage(message: MessageInput, truncateFromTop: boolean): ChannelMessages {
const merged = this.merge([hydrateMessage(this, message, 'preserve')]); const merged = this.merge([hydrateMessage(this, message, 'preserve')]);
if (truncateFromTop) { if (truncateFromTop) {
return merged.truncateTop(IS_MOBILE_CLIENT ? MAX_MESSAGES_PER_CHANNEL : TRUNCATED_MESSAGE_VIEW_SIZE, false); return merged.trimOldest(IS_MOBILE_CLIENT ? MAX_MESSAGES_PER_CHANNEL : TRUNCATED_MESSAGE_VIEW_SIZE, false);
} }
if (this.length > MAX_LOADED_MESSAGES) { if (this.length > MAX_LOADED_MESSAGES) {
return merged.truncateBottom(IS_MOBILE_CLIENT ? MAX_MESSAGES_PER_CHANNEL : TRUNCATED_MESSAGE_VIEW_SIZE, false); return merged.trimNewest(IS_MOBILE_CLIENT ? MAX_MESSAGES_PER_CHANNEL : TRUNCATED_MESSAGE_VIEW_SIZE, false);
} }
return merged; return merged;
} }
receivePushNotification(message: MessageInput): ChannelMessages { applyPushPreview(message: MessageInput): ChannelMessages {
const wire = toWireMessage(message); const wire = toWireMessage(message);
const possibleNonce = wire.nonce ?? null; const possibleNonce = wire.nonce ?? null;
const existing = possibleNonce ? this.get(possibleNonce, true) : null; const existing = possibleNonce ? this.get(possibleNonce, true) : null;
@@ -820,23 +823,23 @@ export class ChannelMessages {
return this.cloneAnd({ready: true, cached: true}).merge([hydrateMessage(this, wire, 'preserve')]); return this.cloneAnd({ready: true, cached: true}).merge([hydrateMessage(this, wire, 'preserve')]);
} }
loadStart(jump?: JumpOptions): ChannelMessages { beginLoad(jump?: JumpOptions): ChannelMessages {
return this.cloneAnd({ return this.cloneAnd({
loadingMore: true, loadingMore: true,
jumped: jump != null, hasJumped: jump != null,
jumpedToPresent: jump?.present ?? false, landedAtLiveEdge: jump?.present ?? false,
jumpTargetId: jump?.messageId ?? null, jumpDestinationId: jump?.messageId ?? null,
jumpTargetOffset: jump?.offset ?? 0, jumpDestinationOffset: jump?.offset ?? 0,
jumpReturnTargetId: jump?.returnMessageId ?? null, jumpReturnMessageId: jump?.returnToMessageId ?? null,
jumpReturnChannelId: jump?.returnMessageId ? (jump.returnChannelId ?? this.channelId) : null, jumpReturnChannelId: jump?.returnToMessageId ? (jump.returnChannelId ?? this.channelId) : null,
jumpReturnGuildId: jump?.returnMessageId ? (jump.returnGuildId ?? null) : null, jumpReturnGuildId: jump?.returnToMessageId ? (jump.returnGuildId ?? null) : null,
ready: jump ? false : this.ready, ready: jump ? false : this.ready,
}); });
} }
loadComplete(options: LoadCompleteOptions): ChannelMessages { applyLoadedWindow(options: LoadCompleteOptions): ChannelMessages {
const { const {
newMessages, windowMessages,
isBefore = false, isBefore = false,
isAfter = false, isAfter = false,
jump = null, jump = null,
@@ -844,7 +847,7 @@ export class ChannelMessages {
hasMoreAfter = false, hasMoreAfter = false,
cached = false, cached = false,
} = options; } = options;
const records = [...newMessages].reverse().map((m) => hydrateMessage(this, m, 'empty')); const records = [...windowMessages].reverse().map((m) => hydrateMessage(this, m, 'empty'));
const loadDecision = resolveChannelMessagesLoadDecision({ const loadDecision = resolveChannelMessagesLoadDecision({
isBefore, isBefore,
isAfter, isAfter,
@@ -853,28 +856,33 @@ export class ChannelMessages {
}); });
let next: ChannelMessages; let next: ChannelMessages;
if (loadDecision.mode === 'replace') { if (loadDecision.mode === 'replace') {
const isWindowReplacement = !isBefore && !isAfter && jump?.messageId == null && jump?.offset == null;
const unsent = isWindowReplacement ? this.collectUnsentMessages(records) : null;
next = this.reset(records); next = this.reset(records);
if (unsent && unsent.length > 0) {
next = next.merge(unsent);
}
} else { } else {
next = this.merge(records, loadDecision.prepend, true); next = this.merge(records, loadDecision.prepend, true);
if (loadDecision.trimBottom) { if (loadDecision.trimBottom) {
next = next.truncate(true, false); next = next.trimToWindow(true, false);
} else if (loadDecision.trimTop) { } else if (loadDecision.trimTop) {
next = next.truncate(false, true); next = next.trimToWindow(false, true);
} }
} }
next = next.cloneAnd({ next = next.cloneAnd({
ready: true, ready: true,
loadingMore: false, loadingMore: false,
jumpType: jump?.jumpType ?? JumpTypes.ANIMATED, jumpType: jump?.jumpType ?? JumpTypes.ANIMATED,
jumpFlash: jump?.flash ?? false, jumpHighlight: jump?.flash ?? false,
jumped: jump != null, hasJumped: jump != null,
jumpedToPresent: jump?.present ?? false, landedAtLiveEdge: jump?.present ?? false,
jumpTargetId: jump?.messageId ?? null, jumpDestinationId: jump?.messageId ?? null,
jumpTargetOffset: jump && jump.messageId != null && jump.offset != null ? jump.offset : 0, jumpDestinationOffset: jump && jump.messageId != null && jump.offset != null ? jump.offset : 0,
jumpSequenceId: jump ? next.jumpSequenceId + 1 : next.jumpSequenceId, jumpTicket: jump ? next.jumpTicket + 1 : next.jumpTicket,
jumpReturnTargetId: jump?.returnMessageId ?? null, jumpReturnMessageId: jump?.returnToMessageId ?? null,
jumpReturnChannelId: jump?.returnMessageId ? (jump.returnChannelId ?? this.channelId) : null, jumpReturnChannelId: jump?.returnToMessageId ? (jump.returnChannelId ?? this.channelId) : null,
jumpReturnGuildId: jump?.returnMessageId ? (jump.returnGuildId ?? null) : null, jumpReturnGuildId: jump?.returnToMessageId ? (jump.returnGuildId ?? null) : null,
hasMoreBefore: loadDecision.preserveHasMoreBefore ? next.hasMoreBefore : hasMoreBefore, hasMoreBefore: loadDecision.preserveHasMoreBefore ? next.hasMoreBefore : hasMoreBefore,
hasMoreAfter: loadDecision.preserveHasMoreAfter ? next.hasMoreAfter : hasMoreAfter, hasMoreAfter: loadDecision.preserveHasMoreAfter ? next.hasMoreAfter : hasMoreAfter,
cached, cached,
@@ -883,6 +891,25 @@ export class ChannelMessages {
return next; return next;
} }
private collectUnsentMessages(records: Array<Message>): Array<Message> {
const unsent = this.messageList.filter((message) => message.hasFailed || message.isSending);
if (unsent.length === 0) return unsent;
const unseenByKey = new Map<string, number>();
for (const record of records) {
if (this.has(record.id)) continue;
const key = unsentCorrelationKey(record);
unseenByKey.set(key, (unseenByKey.get(key) ?? 0) + 1);
}
if (unseenByKey.size === 0) return unsent;
return unsent.filter((message) => {
const key = unsentCorrelationKey(message);
const available = unseenByKey.get(key) ?? 0;
if (available === 0) return true;
unseenByKey.set(key, available - 1);
return false;
});
}
private clearAllMessages(): void { private clearAllMessages(): void {
this.messageList = []; this.messageList = [];
this.messageIndex = {}; this.messageIndex = {};
@@ -927,19 +954,19 @@ export class ChannelMessages {
if (typeof mutator === 'function') { if (typeof mutator === 'function') {
clone.ready = this.ready; clone.ready = this.ready;
clone.jumpType = this.jumpType; clone.jumpType = this.jumpType;
clone.jumpTargetId = this.jumpTargetId; clone.jumpDestinationId = this.jumpDestinationId;
clone.jumpTargetOffset = this.jumpTargetOffset; clone.jumpDestinationOffset = this.jumpDestinationOffset;
clone.jumpSequenceId = this.jumpSequenceId; clone.jumpTicket = this.jumpTicket;
clone.jumped = this.jumped; clone.hasJumped = this.hasJumped;
clone.jumpedToPresent = this.jumpedToPresent; clone.landedAtLiveEdge = this.landedAtLiveEdge;
clone.jumpFlash = this.jumpFlash; clone.jumpHighlight = this.jumpHighlight;
clone.jumpReturnTargetId = this.jumpReturnTargetId; clone.jumpReturnMessageId = this.jumpReturnMessageId;
clone.jumpReturnChannelId = this.jumpReturnChannelId; clone.jumpReturnChannelId = this.jumpReturnChannelId;
clone.jumpReturnGuildId = this.jumpReturnGuildId; clone.jumpReturnGuildId = this.jumpReturnGuildId;
clone.hasMoreBefore = this.hasMoreBefore; clone.hasMoreBefore = this.hasMoreBefore;
clone.hasMoreAfter = this.hasMoreAfter; clone.hasMoreAfter = this.hasMoreAfter;
clone.loadingMore = this.loadingMore; clone.loadingMore = this.loadingMore;
clone.revealedMessageId = this.revealedMessageId; clone.unblurredMessageId = this.unblurredMessageId;
clone.cached = this.cached; clone.cached = this.cached;
clone.error = this.error; clone.error = this.error;
mutator(clone); mutator(clone);
@@ -947,14 +974,16 @@ export class ChannelMessages {
const patch = mutator as Partial<ChannelMessages>; const patch = mutator as Partial<ChannelMessages>;
clone.ready = 'ready' in patch ? !!patch.ready : this.ready; clone.ready = 'ready' in patch ? !!patch.ready : this.ready;
clone.jumpType = patch.jumpType ?? this.jumpType; clone.jumpType = patch.jumpType ?? this.jumpType;
clone.jumpTargetId = 'jumpTargetId' in patch ? (patch.jumpTargetId ?? null) : this.jumpTargetId; clone.jumpDestinationId =
clone.jumpTargetOffset = patch.jumpTargetOffset !== undefined ? patch.jumpTargetOffset : this.jumpTargetOffset; 'jumpDestinationId' in patch ? (patch.jumpDestinationId ?? null) : this.jumpDestinationId;
clone.jumpSequenceId = patch.jumpSequenceId !== undefined ? patch.jumpSequenceId : this.jumpSequenceId; clone.jumpDestinationOffset =
clone.jumped = 'jumped' in patch ? !!patch.jumped : this.jumped; patch.jumpDestinationOffset !== undefined ? patch.jumpDestinationOffset : this.jumpDestinationOffset;
clone.jumpedToPresent = 'jumpedToPresent' in patch ? !!patch.jumpedToPresent : this.jumpedToPresent; clone.jumpTicket = patch.jumpTicket !== undefined ? patch.jumpTicket : this.jumpTicket;
clone.jumpFlash = 'jumpFlash' in patch ? !!patch.jumpFlash : this.jumpFlash; clone.hasJumped = 'hasJumped' in patch ? !!patch.hasJumped : this.hasJumped;
clone.jumpReturnTargetId = clone.landedAtLiveEdge = 'landedAtLiveEdge' in patch ? !!patch.landedAtLiveEdge : this.landedAtLiveEdge;
'jumpReturnTargetId' in patch ? (patch.jumpReturnTargetId ?? null) : this.jumpReturnTargetId; clone.jumpHighlight = 'jumpHighlight' in patch ? !!patch.jumpHighlight : this.jumpHighlight;
clone.jumpReturnMessageId =
'jumpReturnMessageId' in patch ? (patch.jumpReturnMessageId ?? null) : this.jumpReturnMessageId;
clone.jumpReturnChannelId = clone.jumpReturnChannelId =
'jumpReturnChannelId' in patch ? (patch.jumpReturnChannelId ?? null) : this.jumpReturnChannelId; 'jumpReturnChannelId' in patch ? (patch.jumpReturnChannelId ?? null) : this.jumpReturnChannelId;
clone.jumpReturnGuildId = clone.jumpReturnGuildId =
@@ -962,8 +991,8 @@ export class ChannelMessages {
clone.hasMoreBefore = 'hasMoreBefore' in patch ? !!patch.hasMoreBefore : this.hasMoreBefore; clone.hasMoreBefore = 'hasMoreBefore' in patch ? !!patch.hasMoreBefore : this.hasMoreBefore;
clone.hasMoreAfter = 'hasMoreAfter' in patch ? !!patch.hasMoreAfter : this.hasMoreAfter; clone.hasMoreAfter = 'hasMoreAfter' in patch ? !!patch.hasMoreAfter : this.hasMoreAfter;
clone.loadingMore = patch.loadingMore !== undefined ? patch.loadingMore : this.loadingMore; clone.loadingMore = patch.loadingMore !== undefined ? patch.loadingMore : this.loadingMore;
clone.revealedMessageId = clone.unblurredMessageId =
'revealedMessageId' in patch ? (patch.revealedMessageId ?? null) : this.revealedMessageId; 'unblurredMessageId' in patch ? (patch.unblurredMessageId ?? null) : this.unblurredMessageId;
clone.cached = patch.cached ?? this.cached; clone.cached = patch.cached ?? this.cached;
clone.error = patch.error ?? this.error; clone.error = patch.error ?? this.error;
} }
@@ -11,7 +11,6 @@ import {ChannelMessages} from '@app/features/messaging/state/ChannelMessages';
import type {ReactionEmoji} from '@app/features/messaging/utils/ReactionUtils'; import type {ReactionEmoji} from '@app/features/messaging/utils/ReactionUtils';
import SelectedChannel from '@app/features/navigation/state/SelectedChannel'; import SelectedChannel from '@app/features/navigation/state/SelectedChannel';
import SelectedGuild from '@app/features/navigation/state/SelectedGuild'; 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 Relationships from '@app/features/relationship/state/Relationships';
import Dimension from '@app/features/ui/state/Dimension'; import Dimension from '@app/features/ui/state/Dimension';
import Users from '@app/features/user/state/Users'; import Users from '@app/features/user/state/Users';
@@ -39,7 +38,7 @@ export interface PendingJumpDispatch {
messageId: string; messageId: string;
flash?: boolean; flash?: boolean;
offset?: number; offset?: number;
returnTargetId?: string; returnToMessageId?: string;
returnChannelId?: string | null; returnChannelId?: string | null;
returnGuildId?: string | null; returnGuildId?: string | null;
jumpType?: JumpType; jumpType?: JumpType;
@@ -201,6 +200,10 @@ class Messages {
return ChannelMessages.get(channelId); return ChannelMessages.get(channelId);
} }
private hasLoadedPage(messages: ChannelMessages): boolean {
return messages.ready && messages.length > 0;
}
shouldPreloadLatestPage(channelId: string): boolean { shouldPreloadLatestPage(channelId: string): boolean {
if (!GatewayConnection.isConnected || !Channels.getChannel(channelId)) { if (!GatewayConnection.isConnected || !Channels.getChannel(channelId)) {
return false; return false;
@@ -210,13 +213,13 @@ class Messages {
} }
@action @action
preloadLatestPage(channelId: string, guildId?: string | null, unreadAnchor?: UnreadJumpAnchor | null): boolean { preloadLatestPage(channelId: string, guildId?: string | null): boolean {
if (!this.shouldPreloadLatestPage(channelId)) { if (!this.shouldPreloadLatestPage(channelId)) {
return false; return false;
} }
const channel = Channels.getChannel(channelId); const channel = Channels.getChannel(channelId);
const resolvedGuildId = guildId ?? channel?.guildId ?? (channel?.isPrivate() ? ME : undefined); const resolvedGuildId = guildId ?? channel?.guildId ?? (channel?.isPrivate() ? ME : undefined);
this.handleChannelSelect({guildId: resolvedGuildId ?? undefined, channelId, unreadAnchor}); this.handleChannelSelect({guildId: resolvedGuildId ?? undefined, channelId});
return true; return true;
} }
@@ -225,19 +228,19 @@ class Messages {
} }
getLastEditableMessage(channelId: string): Message | undefined { getLastEditableMessage(channelId: string): Message | undefined {
return this.getMessages(channelId).findNewest((message) => { return this.getMessages(channelId).searchFromNewest((message) => {
return message.isCurrentUserAuthor() && message.state === MessageStates.SENT && message.isUserMessage(); return message.isCurrentUserAuthor() && message.state === MessageStates.SENT && message.isUserMessage();
}); });
} }
jumpedMessageId(channelId: string): string | null | undefined { jumpedMessageId(channelId: string): string | null | undefined {
const channel = ChannelMessages.get(channelId); const channel = ChannelMessages.get(channelId);
return channel?.jumpTargetId; return channel?.jumpDestinationId;
} }
hasPresent(channelId: string): boolean { hasNewestMessages(channelId: string): boolean {
const channel = ChannelMessages.get(channelId); const channel = ChannelMessages.get(channelId);
return channel?.hasPresent() ?? false; return channel?.hasNewestMessages() ?? false;
} }
@action @action
@@ -257,7 +260,7 @@ class Messages {
let didUpdate = false; let didUpdate = false;
ChannelMessages.forEach((messages) => { ChannelMessages.forEach((messages) => {
if (messages.loadingMore) { if (messages.loadingMore) {
this.commitMessages(messages.mutate({loadingMore: false})); this.commitMessages(messages.withPatch({loadingMore: false}));
didUpdate = true; didUpdate = true;
} }
}); });
@@ -286,14 +289,14 @@ class Messages {
@action @action
handleResumed(): boolean { handleResumed(): boolean {
ChannelMessages.forEach((messages) => { ChannelMessages.forEach((messages) => {
this.commitMessages(messages.mutate({ready: true})); this.commitMessages(messages.withPatch({ready: true}));
}); });
this.notifyChange(); this.notifyChange();
return true; return true;
} }
@action @action
handleConnectionOpen(resolveUnreadAnchor: (channelId: string) => UnreadJumpAnchor | null): boolean { handleConnectionOpen(): boolean {
const selectedChannelId = SelectedChannel.currentChannelId; const selectedChannelId = SelectedChannel.currentChannelId;
let didHydrateSelectedChannel = false; let didHydrateSelectedChannel = false;
if (selectedChannelId) { if (selectedChannelId) {
@@ -307,14 +310,10 @@ class Messages {
} }
ChannelMessages.forEach((messages) => { ChannelMessages.forEach((messages) => {
if (messages.channelId === selectedChannelId && Channels.getChannel(messages.channelId) != null) { if (messages.channelId === selectedChannelId && Channels.getChannel(messages.channelId) != null) {
this.startChannelHydration(messages.channelId, { this.startChannelHydration(messages.channelId, {forceScrollToBottom: this.pendingFullHydration});
forceScrollToBottom: this.pendingFullHydration,
unreadAnchor: resolveUnreadAnchor(messages.channelId),
});
didHydrateSelectedChannel = true; didHydrateSelectedChannel = true;
} else { } else {
this.clearMessages(messages.channelId); this.commitMessages(messages.withPatch({ready: false, loadingMore: false}));
Dimension.forgetChannelDimensions(messages.channelId);
} }
}); });
if (this.pendingFullHydration && !didHydrateSelectedChannel && selectedChannelId) { if (this.pendingFullHydration && !didHydrateSelectedChannel && selectedChannelId) {
@@ -325,19 +324,8 @@ class Messages {
if (!didHydrateSelectedChannel && selectedChannelId && Channels.getChannel(selectedChannelId)) { if (!didHydrateSelectedChannel && selectedChannelId && Channels.getChannel(selectedChannelId)) {
const messages = ChannelMessages.getOrCreate(selectedChannelId); const messages = ChannelMessages.getOrCreate(selectedChannelId);
if (!messages.ready && !messages.loadingMore && messages.length === 0) { if (!messages.ready && !messages.loadingMore && messages.length === 0) {
this.commitMessages(messages.mutate({loadingMore: true})); this.commitMessages(messages.withPatch({loadingMore: true}));
const unreadAnchor = resolveUnreadAnchor(selectedChannelId); MessageCommands.fetchMessages(selectedChannelId, null, null, MAX_MESSAGES_PER_CHANNEL);
if (unreadAnchor != null) {
MessageCommands.jumpToMessage({
channelId: selectedChannelId,
messageId: unreadAnchor.messageId,
offset: unreadAnchor.offset,
flash: false,
jumpType: JumpTypes.INSTANT,
});
} else {
MessageCommands.fetchMessages(selectedChannelId, null, null, MAX_MESSAGES_PER_CHANNEL);
}
didHydrateSelectedChannel = true; didHydrateSelectedChannel = true;
} }
} }
@@ -345,42 +333,19 @@ class Messages {
return didHydrateSelectedChannel; return didHydrateSelectedChannel;
} }
private startChannelHydration( private startChannelHydration(channelId: string, options: {forceScrollToBottom?: boolean} = {}): void {
channelId: string,
options: {
forceScrollToBottom?: boolean;
unreadAnchor?: UnreadJumpAnchor | null;
} = {},
): void {
if (!Channels.getChannel(channelId)) return; if (!Channels.getChannel(channelId)) return;
const {forceScrollToBottom = false, unreadAnchor = null} = options; const {forceScrollToBottom = false} = options;
const messages = ChannelMessages.getOrCreate(channelId); const messages = ChannelMessages.getOrCreate(channelId);
this.commitMessages(messages.mutate({loadingMore: true, ready: false, error: false})); this.commitMessages(messages.withPatch({loadingMore: true, ready: false, error: false}));
if (forceScrollToBottom) { if (forceScrollToBottom) {
Dimension.recordChannelDimensions(channelId, 1, 1, 0); Dimension.recordChannelDimensions(channelId, 1, 1, 0);
MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL);
return;
}
if (unreadAnchor != null) {
MessageCommands.jumpToMessage({
channelId,
messageId: unreadAnchor.messageId,
offset: unreadAnchor.offset,
flash: false,
jumpType: JumpTypes.INSTANT,
});
return;
} }
MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL); MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL);
} }
@action @action
handleChannelSelect(action: { handleChannelSelect(action: {guildId?: string; channelId?: string | null; messageId?: string}): boolean {
guildId?: string;
channelId?: string | null;
messageId?: string;
unreadAnchor?: UnreadJumpAnchor | null;
}): boolean {
const channelId = action.channelId ?? action.guildId; const channelId = action.channelId ?? action.guildId;
if (channelId == null || channelId === ME) { if (channelId == null || channelId === ME) {
return false; return false;
@@ -401,9 +366,9 @@ class Messages {
MessageCommands.jumpToMessage({channelId, ...dispatch}); MessageCommands.jumpToMessage({channelId, ...dispatch});
return false; return false;
} }
if (!GatewayConnection.isConnected || messages.loadingMore || messages.ready) { if (!GatewayConnection.isConnected || messages.loadingMore || this.hasLoadedPage(messages)) {
if (messages.ready && Dimension.channelPinnedToEnd(channelId)) { if (this.hasLoadedPage(messages) && Dimension.channelPinnedToEnd(channelId)) {
this.commitMessages(messages.truncateTop(MAX_MESSAGES_PER_CHANNEL)); this.commitMessages(messages.trimOldest(MAX_MESSAGES_PER_CHANNEL));
this.notifyChange(); this.notifyChange();
} }
return false; return false;
@@ -421,19 +386,8 @@ class Messages {
if (!isNonGuildChannel && !guildExists) { if (!isNonGuildChannel && !guildExists) {
return false; return false;
} }
this.commitMessages(messages.mutate({loadingMore: true})); this.commitMessages(messages.withPatch({loadingMore: true}));
this.notifyChange(); 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); MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL);
return false; return false;
} }
@@ -486,7 +440,7 @@ class Messages {
messageId: undefined, messageId: undefined,
}); });
if (!didChannelSelect && currentMessages && currentMessages.length === 0 && !currentMessages.ready) { if (!didChannelSelect && currentMessages && currentMessages.length === 0 && !currentMessages.ready) {
this.commitMessages(currentMessages.mutate({loadingMore: true})); this.commitMessages(currentMessages.withPatch({loadingMore: true}));
MessageCommands.fetchMessages(selectedChannelId, null, null, MAX_MESSAGES_PER_CHANNEL); MessageCommands.fetchMessages(selectedChannelId, null, null, MAX_MESSAGES_PER_CHANNEL);
this.notifyChange(); this.notifyChange();
return true; return true;
@@ -497,16 +451,16 @@ class Messages {
@action @action
handleLoadMessages(action: {channelId: string; jump?: JumpOptions}): boolean { handleLoadMessages(action: {channelId: string; jump?: JumpOptions}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId); const messages = ChannelMessages.getOrCreate(action.channelId);
this.commitMessages(messages.loadStart(action.jump)); this.commitMessages(messages.beginLoad(action.jump));
this.notifyChange(); this.notifyChange();
return false; return false;
} }
@action @action
handleTruncateMessages(action: {channelId: string; truncateBottom?: boolean; truncateTop?: boolean}): boolean { handleTruncateMessages(action: {channelId: string; trimNewest?: boolean; trimOldest?: boolean}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId).truncate( const messages = ChannelMessages.getOrCreate(action.channelId).trimToWindow(
action.truncateBottom ?? false, action.trimNewest ?? false,
action.truncateTop ?? false, action.trimOldest ?? false,
); );
this.commitMessages(messages); this.commitMessages(messages);
this.notifyChange(); this.notifyChange();
@@ -523,19 +477,19 @@ class Messages {
}): boolean { }): boolean {
let messages = ChannelMessages.getOrCreate(action.channelId); let messages = ChannelMessages.getOrCreate(action.channelId);
if (action.jump?.present) { if (action.jump?.present) {
messages = messages.jumpToPresent(action.limit); messages = messages.jumpToLiveEdge(action.limit);
} else if (action.jump?.messageId) { } else if (action.jump?.messageId) {
messages = messages.jumpToMessage({ messages = messages.jumpToMessage({
messageId: action.jump.messageId, messageId: action.jump.messageId,
flash: action.jump.flash, flash: action.jump.flash,
offset: action.jump.offset, offset: action.jump.offset,
returnTargetId: action.jump.returnMessageId, returnToMessageId: action.jump.returnToMessageId,
returnChannelId: action.jump.returnChannelId, returnChannelId: action.jump.returnChannelId,
returnGuildId: action.jump.returnGuildId, returnGuildId: action.jump.returnGuildId,
jumpType: action.jump.jumpType, jumpType: action.jump.jumpType,
}); });
} else if (action.before || action.after) { } else if (action.before || action.after) {
messages = messages.loadFromCache(action.before != null, action.limit); messages = messages.drainBufferedSide(action.before != null, action.limit);
} }
this.commitMessages(messages); this.commitMessages(messages);
this.notifyChange(); this.notifyChange();
@@ -553,8 +507,8 @@ class Messages {
cached?: boolean; cached?: boolean;
messages: Array<WireMessage>; messages: Array<WireMessage>;
}): boolean { }): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId).loadComplete({ const messages = ChannelMessages.getOrCreate(action.channelId).applyLoadedWindow({
newMessages: action.messages, windowMessages: action.messages,
isBefore: action.isBefore, isBefore: action.isBefore,
isAfter: action.isAfter, isAfter: action.isAfter,
jump: action.jump, jump: action.jump,
@@ -570,7 +524,7 @@ class Messages {
@action @action
handleLoadMessagesFailure(action: {channelId: string}): boolean { handleLoadMessagesFailure(action: {channelId: string}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId); const messages = ChannelMessages.getOrCreate(action.channelId);
this.commitMessages(messages.mutate({loadingMore: false, error: true})); this.commitMessages(messages.withPatch({loadingMore: false, error: true}));
this.notifyChange(); this.notifyChange();
return false; return false;
} }
@@ -581,7 +535,7 @@ class Messages {
if (!messages.loadingMore && !messages.error) { if (!messages.loadingMore && !messages.error) {
return false; return false;
} }
this.commitMessages(messages.mutate({loadingMore: false, error: false})); this.commitMessages(messages.withPatch({loadingMore: false, error: false}));
this.notifyChange(); this.notifyChange();
return true; return true;
} }
@@ -593,7 +547,7 @@ class Messages {
if (!existing?.ready) { if (!existing?.ready) {
return false; return false;
} }
const updated = existing.receiveMessage(action.message, Dimension.channelPinnedToEnd(action.channelId)); const updated = existing.applyIncomingMessage(action.message, Dimension.channelPinnedToEnd(action.channelId));
this.commitMessages(updated); this.commitMessages(updated);
this.notifyChange(); this.notifyChange();
return false; return false;
@@ -626,9 +580,9 @@ class Messages {
return false; return false;
} }
let messages = existing; let messages = existing;
if (messages.revealedMessageId === action.id) { if (messages.unblurredMessageId === action.id) {
const messageAfter = messages.getAfter(action.id); const messageAfter = messages.nextAfter(action.id);
messages = messages.mutate({revealedMessageId: messageAfter?.id ?? null}); messages = messages.withPatch({unblurredMessageId: messageAfter?.id ?? null});
} }
messages = messages.remove(action.id); messages = messages.remove(action.id);
this.commitMessages(messages); this.commitMessages(messages);
@@ -640,11 +594,11 @@ class Messages {
handleMessageDeleteBulk(action: {ids: Array<string>; channelId: string}): boolean { handleMessageDeleteBulk(action: {ids: Array<string>; channelId: string}): boolean {
const existing = ChannelMessages.get(action.channelId); const existing = ChannelMessages.get(action.channelId);
if (!existing) return false; if (!existing) return false;
let messages = existing.removeMany(action.ids); let messages = existing.removeIds(action.ids);
if (messages === existing) return false; if (messages === existing) return false;
if (messages.revealedMessageId != null && action.ids.includes(messages.revealedMessageId)) { if (messages.unblurredMessageId != null && action.ids.includes(messages.unblurredMessageId)) {
const after = messages.getAfter(messages.revealedMessageId); const after = messages.nextAfter(messages.unblurredMessageId);
messages = messages.mutate({revealedMessageId: after?.id ?? null}); messages = messages.withPatch({unblurredMessageId: after?.id ?? null});
} }
this.commitMessages(messages); this.commitMessages(messages);
this.notifyChange(); this.notifyChange();
@@ -752,7 +706,7 @@ class Messages {
@action @action
handleMessageReveal(action: {channelId: string; messageId: string | null}): boolean { handleMessageReveal(action: {channelId: string; messageId: string | null}): boolean {
const messages = ChannelMessages.getOrCreate(action.channelId); const messages = ChannelMessages.getOrCreate(action.channelId);
this.commitMessages(messages.mutate({revealedMessageId: action.messageId})); this.commitMessages(messages.withPatch({unblurredMessageId: action.messageId}));
this.notifyChange(); this.notifyChange();
return true; return true;
} }
@@ -762,9 +716,9 @@ class Messages {
const messages = ChannelMessages.get(action.channelId); const messages = ChannelMessages.get(action.channelId);
if ( if (
messages && messages &&
(messages.jumpTargetId != null || (messages.jumpDestinationId != null ||
messages.jumped || messages.hasJumped ||
(action.clearReturnTarget === true && messages.jumpReturnTargetId != null)) (action.clearReturnTarget === true && messages.jumpReturnMessageId != null))
) { ) {
this.commitMessages(messages.clearJumpTarget({clearReturnTarget: action.clearReturnTarget})); this.commitMessages(messages.clearJumpTarget({clearReturnTarget: action.clearReturnTarget}));
this.notifyChange(); this.notifyChange();
@@ -829,7 +783,7 @@ class Messages {
Channels.handleMessageCreate({message: messageData}); Channels.handleMessageCreate({message: messageData});
const channelMessages = ChannelMessages.getOrCreate(channelId); const channelMessages = ChannelMessages.getOrCreate(channelId);
if (!channelMessages.has(messageData.id)) { if (!channelMessages.has(messageData.id)) {
this.commitMessages(channelMessages.receiveMessage(messageData, false)); this.commitMessages(channelMessages.applyIncomingMessage(messageData, false));
hasChanges = true; hasChanges = true;
} }
} }
@@ -234,11 +234,11 @@ export function createChannelStream(props: {
if (groupId === message.id) { if (groupId === message.id) {
lastMessageInGroup = message; lastMessageInGroup = message;
} }
const {jumpSequenceId, jumpFlash, jumpTargetId} = messages; const {jumpTicket, jumpHighlight, jumpDestinationId} = messages;
if (jumpFlash && message.id === jumpTargetId && jumpSequenceId != null) { if (jumpHighlight && message.id === jumpDestinationId && jumpTicket != null) {
messageItem.flashKey = jumpSequenceId; messageItem.flashKey = jumpTicket;
} }
if (messages.jumpTargetId === message.id) { if (messages.jumpDestinationId === message.id) {
messageItem.jumpTarget = true; messageItem.jumpTarget = true;
} }
if (collapsedGroupItem !== null) { if (collapsedGroupItem !== null) {
@@ -11,7 +11,7 @@ import {type JumpType, JumpTypes} from '@fluxer/constants/src/JumpConstants';
interface MessageJumpOptions { interface MessageJumpOptions {
flash?: boolean; flash?: boolean;
offset?: number; offset?: number;
returnTargetId?: string; returnToMessageId?: string;
returnChannelId?: string | null; returnChannelId?: string | null;
returnGuildId?: string | null; returnGuildId?: string | null;
jumpType?: JumpType; jumpType?: JumpType;
@@ -34,20 +34,20 @@ function resolveReturnGuildId(
} }
function resolveReturnChannelId( function resolveReturnChannelId(
returnTargetId: string | undefined, returnToMessageId: string | undefined,
explicitChannelId: string | null | undefined, explicitChannelId: string | null | undefined,
): string | null | undefined { ): string | null | undefined {
if (!returnTargetId) return undefined; if (!returnToMessageId) return undefined;
if (explicitChannelId != null) return explicitChannelId; if (explicitChannelId != null) return explicitChannelId;
return Navigation.messageId === returnTargetId ? Navigation.channelId : undefined; return Navigation.messageId === returnToMessageId ? Navigation.channelId : undefined;
} }
function resolveSameChannelReturnChannelId( function resolveSameChannelReturnChannelId(
targetChannelId: string, targetChannelId: string,
returnTargetId: string | undefined, returnToMessageId: string | undefined,
explicitReturnChannelId: string | null | undefined, explicitReturnChannelId: string | null | undefined,
): string | undefined { ): string | undefined {
const returnChannelId = resolveReturnChannelId(returnTargetId, explicitReturnChannelId); const returnChannelId = resolveReturnChannelId(returnToMessageId, explicitReturnChannelId);
return returnChannelId === targetChannelId ? returnChannelId : undefined; return returnChannelId === targetChannelId ? returnChannelId : undefined;
} }
@@ -56,16 +56,16 @@ export function goToMessage(channelId: string, messageId: string, options?: Mess
const guildId = resolveGuildId(channelId, options?.viewContext); const guildId = resolveGuildId(channelId, options?.viewContext);
const returnChannelId = resolveSameChannelReturnChannelId( const returnChannelId = resolveSameChannelReturnChannelId(
channelId, channelId,
options?.returnTargetId, options?.returnToMessageId,
options?.returnChannelId, options?.returnChannelId,
); );
const returnTargetId = returnChannelId ? options?.returnTargetId : undefined; const returnToMessageId = returnChannelId ? options?.returnToMessageId : undefined;
const returnGuildId = returnTargetId ? resolveReturnGuildId(returnChannelId, options?.returnGuildId) : undefined; const returnGuildId = returnToMessageId ? resolveReturnGuildId(returnChannelId, options?.returnGuildId) : undefined;
const dispatch = { const dispatch = {
messageId, messageId,
flash: options?.flash ?? true, flash: options?.flash ?? true,
offset: options?.offset, offset: options?.offset,
returnTargetId, returnToMessageId,
returnChannelId, returnChannelId,
returnGuildId, returnGuildId,
jumpType: isSameChannel ? (options?.jumpType ?? JumpTypes.ANIMATED) : JumpTypes.INSTANT, jumpType: isSameChannel ? (options?.jumpType ?? JumpTypes.ANIMATED) : JumpTypes.INSTANT,
@@ -48,7 +48,7 @@ export interface ScrollManagerProps {
handleScrollFromBottom: () => void; handleScrollFromBottom: () => void;
additionalMessagePadding: number; additionalMessagePadding: number;
canAutoAck: boolean; canAutoAck: boolean;
handleJumpHighlight: (messageId: string | null, jumpSequenceId: number) => void; handleJumpHighlight: (messageId: string | null, jumpTicket: number) => void;
} }
export class ScrollManager { export class ScrollManager {
@@ -83,7 +83,7 @@ export class ScrollManager {
} | null = null; } | null = null;
private loadLastDirection: 'before' | 'after' | null = null; private loadLastDirection: 'before' | 'after' | null = null;
private pinPreUpdateState: ScrollPinResult | null = null; private pinPreUpdateState: ScrollPinResult | null = null;
private jumpSequenceId: number | null = null; private jumpTicket: number | null = null;
private jumpCallbackToken = 0; private jumpCallbackToken = 0;
private anchorAutomaticListeners: Array<(anchor: AnchorData | null, bottom: AnchorData | null) => void> = []; private anchorAutomaticListeners: Array<(anchor: AnchorData | null, bottom: AnchorData | null) => void> = [];
private dimensionPersistDebounced: DebouncedFunction<() => void>; private dimensionPersistDebounced: DebouncedFunction<() => void>;
@@ -91,7 +91,7 @@ export class ScrollManager {
constructor(props: ScrollManagerProps) { constructor(props: ScrollManagerProps) {
this.props = props; this.props = props;
this.loadIsActive = props.messages.loadingMore; this.loadIsActive = props.messages.loadingMore;
if (props.messages.jumpTargetId != null) { if (props.messages.jumpDestinationId != null) {
this.pinIsAtBottom = false; this.pinIsAtBottom = false;
} else { } else {
const stored = Dimension.channelDimensionsFor(props.channel.id); const stored = Dimension.channelDimensionsFor(props.channel.id);
@@ -143,10 +143,10 @@ export class ScrollManager {
); );
} }
private jumpBegin(jumpSequenceId: number | null): number { private jumpBegin(jumpTicket: number | null): number {
this.jumpCallbackToken += 1; this.jumpCallbackToken += 1;
this.jumpIsActive = true; this.jumpIsActive = true;
this.jumpSequenceId = jumpSequenceId; this.jumpTicket = jumpTicket;
return this.jumpCallbackToken; return this.jumpCallbackToken;
} }
@@ -160,7 +160,7 @@ export class ScrollManager {
this.ref.current?.cancelScroll(); this.ref.current?.cancelScroll();
} }
this.jumpIsActive = false; this.jumpIsActive = false;
this.jumpSequenceId = null; this.jumpTicket = null;
} }
private pinComputeState(state: ScrollerState): ScrollPinResult { private pinComputeState(state: ScrollerState): ScrollPinResult {
@@ -310,7 +310,7 @@ export class ScrollManager {
if (idx === -1) { if (idx === -1) {
return channel.id; return channel.id;
} }
return messages.getByIndex(idx)?.id; return messages.atIndex(idx)?.id;
}; };
while (true) { while (true) {
const messageId = getMessageId(index); const messageId = getMessageId(index);
@@ -341,8 +341,8 @@ export class ScrollManager {
const direction = isBefore ? 1 : -1; const direction = isBefore ? 1 : -1;
const startIndex = isBefore ? 0 : messages.length - 1; const startIndex = isBefore ? 0 : messages.length - 1;
let anchor: AnchorData | null = null; let anchor: AnchorData | null = null;
for (let i = startIndex; messages.getByIndex(i) != null; i += direction) { for (let i = startIndex; messages.atIndex(i) != null; i += direction) {
const msg = messages.getByIndex(i)!; const msg = messages.atIndex(i)!;
const data = this.anchorGetData(msg.id, scrollTop, undefined, layout); const data = this.anchorGetData(msg.id, scrollTop, undefined, layout);
if (data) { if (data) {
anchor = data; anchor = data;
@@ -388,7 +388,7 @@ export class ScrollManager {
return; return;
} }
this.jumpIsActive = false; this.jumpIsActive = false;
this.jumpSequenceId = null; this.jumpTicket = null;
this.jumpCallbackToken += 1; this.jumpCallbackToken += 1;
const state = this.scrollGetState(); const state = this.scrollGetState();
const pinState = this.pinComputeState(state); const pinState = this.pinComputeState(state);
@@ -828,7 +828,7 @@ export class ScrollManager {
private jumpFixTarget(): void { private jumpFixTarget(): void {
const {messages} = this.props; const {messages} = this.props;
const jumpToken = this.jumpCallbackToken; const jumpToken = this.jumpCallbackToken;
const targetId = messages.jumpTargetId ? resolveJumpTargetId(messages) : null; const targetId = messages.jumpDestinationId ? resolveJumpTargetId(messages) : null;
if (targetId) { if (targetId) {
const element = this.layoutGetElementFromMessageId(targetId); const element = this.layoutGetElementFromMessageId(targetId);
if (element) { if (element) {
@@ -852,7 +852,7 @@ export class ScrollManager {
callback?: () => void, callback?: () => void,
animate = true, animate = true,
suppressPadding = false, suppressPadding = false,
jumpToken = this.jumpBegin(this.jumpSequenceId), jumpToken = this.jumpBegin(this.jumpTicket),
): void { ): void {
const doc = this.scrollGetDocument(); const doc = this.scrollGetDocument();
const newMessagesBar = doc?.getElementById('new-messages-bar'); const newMessagesBar = doc?.getElementById('new-messages-bar');
@@ -862,7 +862,7 @@ export class ScrollManager {
return; return;
} }
this.jumpIsActive = false; this.jumpIsActive = false;
this.jumpSequenceId = null; this.jumpTicket = null;
this.anchorSetAutomatic(this.anchorFindTopVisible()); this.anchorSetAutomatic(this.anchorFindTopVisible());
callback?.(); callback?.();
if (this.jumpIsCurrent(jumpToken)) { if (this.jumpIsCurrent(jumpToken)) {
@@ -965,13 +965,13 @@ export class ScrollManager {
this.jumpStop(true); this.jumpStop(true);
Dimension.recordChannelDimensions(channel.id, 1, 1, 0); Dimension.recordChannelDimensions(channel.id, 1, 1, 0);
if (messages.hasMoreAfter) { if (messages.hasMoreAfter) {
MessageCommands.jumpToPresent(channel.id, MAX_MESSAGES_PER_CHANNEL); MessageCommands.jumpToLiveEdge(channel.id, MAX_MESSAGES_PER_CHANNEL);
} else { } else {
this.pinIsScrollingProgrammatically = true; this.pinIsScrollingProgrammatically = true;
this.pinIsAtBottom = true; this.pinIsAtBottom = true;
this.scrollTo(Number.MAX_SAFE_INTEGER, animate, () => { this.scrollTo(Number.MAX_SAFE_INTEGER, animate, () => {
this.jumpIsActive = false; this.jumpIsActive = false;
this.jumpSequenceId = null; this.jumpTicket = null;
this.pinIsScrollingProgrammatically = false; this.pinIsScrollingProgrammatically = false;
this.scrollHandle(); this.scrollHandle();
}); });
@@ -1001,8 +1001,8 @@ export class ScrollManager {
return false; return false;
} }
const {channel, messages} = this.props; const {channel, messages} = this.props;
const returnTargetId = messages.jumpReturnTargetId; const returnToMessageId = messages.jumpReturnMessageId;
if (!returnTargetId) { if (!returnToMessageId) {
return false; return false;
} }
const returnChannelId = messages.jumpReturnChannelId ?? channel.id; const returnChannelId = messages.jumpReturnChannelId ?? channel.id;
@@ -1016,11 +1016,11 @@ export class ScrollManager {
this.pinIsScrollingProgrammatically = false; this.pinIsScrollingProgrammatically = false;
Messages.handleClearJumpTarget({channelId: channel.id, clearReturnTarget: true}); Messages.handleClearJumpTarget({channelId: channel.id, clearReturnTarget: true});
const dispatch = { const dispatch = {
messageId: returnTargetId, messageId: returnToMessageId,
flash: false, flash: false,
jumpType: JumpTypes.INSTANT, jumpType: JumpTypes.INSTANT,
}; };
if (Navigation.channelId === returnChannelId && Navigation.messageId === returnTargetId) { if (Navigation.channelId === returnChannelId && Navigation.messageId === returnToMessageId) {
MessageCommands.jumpToMessage({ MessageCommands.jumpToMessage({
channelId: returnChannelId, channelId: returnChannelId,
...dispatch, ...dispatch,
@@ -1028,12 +1028,12 @@ export class ScrollManager {
return true; return true;
} }
Messages.setPendingJumpDispatch(returnChannelId, dispatch); Messages.setPendingJumpDispatch(returnChannelId, dispatch);
NavigationCommands.navigateToMessage(returnGuildId, returnChannelId, returnTargetId, 'replace'); NavigationCommands.navigateToMessage(returnGuildId, returnChannelId, returnToMessageId, 'replace');
return true; return true;
} }
jumpCancel(): boolean { jumpCancel(): boolean {
if (this.lifecycleIsDisposed || (!this.jumpIsActive && this.props.messages.jumpTargetId == null)) { if (this.lifecycleIsDisposed || (!this.jumpIsActive && this.props.messages.jumpDestinationId == null)) {
return false; return false;
} }
this.jumpStop(true); this.jumpStop(true);
@@ -1135,9 +1135,9 @@ export class ScrollManager {
scrollToMessage(messageId: string, animate = true): void { scrollToMessage(messageId: string, animate = true): void {
const scrollerHandle = this.ref.current; const scrollerHandle = this.ref.current;
if (!scrollerHandle) return; if (!scrollerHandle) return;
const {jumpFlash, jumpSequenceId, jumpTargetId} = this.props.messages; const {jumpHighlight, jumpTicket, jumpDestinationId} = this.props.messages;
const highlightTargetId = jumpFlash && jumpTargetId !== this.props.channel.id ? jumpTargetId : null; const highlightTargetId = jumpHighlight && jumpDestinationId !== this.props.channel.id ? jumpDestinationId : null;
this.props.handleJumpHighlight(highlightTargetId, jumpSequenceId); this.props.handleJumpHighlight(highlightTargetId, jumpTicket);
if (messageId === this.props.channel.id) { if (messageId === this.props.channel.id) {
this.scrollTo(0); this.scrollTo(0);
return; return;
@@ -1145,7 +1145,7 @@ export class ScrollManager {
const element = this.layoutGetElementFromMessageId(messageId); const element = this.layoutGetElementFromMessageId(messageId);
const scrollerNode = scrollerHandle.getViewportElement(); const scrollerNode = scrollerHandle.getViewportElement();
this.pinIsAtBottom = false; this.pinIsAtBottom = false;
const jumpToken = this.jumpBegin(this.props.messages.jumpSequenceId); const jumpToken = this.jumpBegin(this.props.messages.jumpTicket);
const shouldContinue = () => this.jumpIsCurrent(jumpToken); const shouldContinue = () => this.jumpIsCurrent(jumpToken);
const onComplete = () => this.jumpComplete(element, jumpToken); const onComplete = () => this.jumpComplete(element, jumpToken);
if (!element || !scrollerNode) { if (!element || !scrollerNode) {
@@ -1204,33 +1204,33 @@ export class ScrollManager {
return; return;
} }
} else { } else {
if (nextProps.messages.jumpTargetId == null) { if (nextProps.messages.jumpDestinationId == null) {
this.scrollTo(Number.MAX_SAFE_INTEGER); this.scrollTo(Number.MAX_SAFE_INTEGER);
} }
return; return;
} }
if (nextProps.messages.jumpTargetId != null) { if (nextProps.messages.jumpDestinationId != null) {
if (this.loadIsInProgress()) { if (this.loadIsInProgress()) {
return; return;
} }
const targetId = resolveJumpTargetId(nextProps.messages); const targetId = resolveJumpTargetId(nextProps.messages);
if (targetId == null) { if (targetId == null) {
nextProps.handleJumpHighlight(null, nextProps.messages.jumpSequenceId); nextProps.handleJumpHighlight(null, nextProps.messages.jumpTicket);
this.jumpStop(); this.jumpStop();
return; return;
} }
if (this.jumpSequenceId === nextProps.messages.jumpSequenceId) { if (this.jumpTicket === nextProps.messages.jumpTicket) {
return; return;
} }
if (nextProps.messages.jumpSequenceId === prevMessages.jumpSequenceId && this.jumpIsActiveNow()) { if (nextProps.messages.jumpTicket === prevMessages.jumpTicket && this.jumpIsActiveNow()) {
return; return;
} }
this.scrollToMessage(targetId, shouldAnimateMessageJump(nextProps.messages.jumpType)); this.scrollToMessage(targetId, shouldAnimateMessageJump(nextProps.messages.jumpType));
return; return;
} }
if (nextProps.messages.jumpedToPresent && prevMessages.jumpSequenceId !== nextProps.messages.jumpSequenceId) { if (nextProps.messages.jumpedToPresent && prevMessages.jumpTicket !== nextProps.messages.jumpTicket) {
nextProps.handleJumpHighlight(null, nextProps.messages.jumpSequenceId); nextProps.handleJumpHighlight(null, nextProps.messages.jumpTicket);
this.jumpBegin(nextProps.messages.jumpSequenceId); this.jumpBegin(nextProps.messages.jumpTicket);
this.scrollTo(0); this.scrollTo(0);
this.scrollSetToBottom(); this.scrollSetToBottom();
return; return;
@@ -1290,7 +1290,7 @@ export class ScrollManager {
this.anchorFetch = null; this.anchorFetch = null;
this.anchorFetchFallback = null; this.anchorFetchFallback = null;
this.pinPreUpdateState = null; this.pinPreUpdateState = null;
this.jumpSequenceId = null; this.jumpTicket = null;
this.jumpCallbackToken += 1; this.jumpCallbackToken += 1;
for (const cb of this.anchorAutomaticListeners) { for (const cb of this.anchorAutomaticListeners) {
this.anchorRemoveAutomaticListener(cb); this.anchorRemoveAutomaticListener(cb);
@@ -75,19 +75,19 @@ export function shouldAnimateMessageJump(jumpType: JumpType): boolean {
} }
export function resolveJumpTargetId(messages: ChannelMessages): string | null { export function resolveJumpTargetId(messages: ChannelMessages): string | null {
const {jumpTargetId, jumpTargetOffset} = messages; const {jumpDestinationId, jumpDestinationOffset} = messages;
if (!jumpTargetId || !messages.ready) return null; if (!jumpDestinationId || !messages.ready) return null;
if (messages.has(jumpTargetId) || (!messages.hasMoreBefore && jumpTargetId === messages.channelId)) { if (messages.has(jumpDestinationId) || (!messages.hasMoreBefore && jumpDestinationId === messages.channelId)) {
if (jumpTargetOffset === 0) { if (jumpDestinationOffset === 0) {
return jumpTargetId; return jumpDestinationId;
} }
const index = messages.indexOf(jumpTargetId); const index = messages.indexOf(jumpDestinationId);
const targetMessage = messages.getByIndex(index + jumpTargetOffset); const targetMessage = messages.atIndex(index + jumpDestinationOffset);
return targetMessage?.id ?? jumpTargetId; return targetMessage?.id ?? jumpDestinationId;
} }
const allIds = [jumpTargetId, ...messages.map((m) => m.id)].sort(compare); const allIds = [jumpDestinationId, ...messages.map((m) => m.id)].sort(compare);
const jumpIndex = allIds.indexOf(jumpTargetId); const jumpIndex = allIds.indexOf(jumpDestinationId);
const offset = Math.abs(jumpTargetOffset) > 0 ? jumpTargetOffset : 1; const offset = Math.abs(jumpDestinationOffset) > 0 ? jumpDestinationOffset : 1;
const closestId = allIds[jumpIndex + offset] ?? allIds[jumpIndex - 1]; const closestId = allIds[jumpIndex + offset] ?? allIds[jumpIndex - 1];
return closestId ?? null; return closestId ?? null;
} }
@@ -240,7 +240,7 @@ export class ReadStateEntry {
let loadedOlderMessages = false; let loadedOlderMessages = false;
let oldestUnread: string | null = null; let oldestUnread: string | null = null;
let loadedUnreadCount = 0; let loadedUnreadCount = 0;
messages.forAll((message) => { messages.forEachBuffered((message) => {
if (!foundAckMessage) { if (!foundAckMessage) {
foundAckMessage = message.id === this.storedAckMessageId; foundAckMessage = message.id === this.storedAckMessageId;
} else if (this.storedOldestUnreadMessageId == null) { } else if (this.storedOldestUnreadMessageId == null) {
@@ -273,8 +273,8 @@ export class ReadStateEntry {
} }
}); });
const hasUnreadBoundary = foundAckMessage || loadedOlderMessages || !messages.hasMoreBefore; const hasUnreadBoundary = foundAckMessage || loadedOlderMessages || !messages.hasMoreBefore;
const hasPresent = messages.hasPresent(); const hasNewestMessages = messages.hasNewestMessages();
this.estimated = !hasPresent || !hasUnreadBoundary; this.estimated = !hasNewestMessages || !hasUnreadBoundary;
if (this.estimated) { if (this.estimated) {
this.unreadCount = Math.max(previousUnreadCount, loadedUnreadCount); this.unreadCount = Math.max(previousUnreadCount, loadedUnreadCount);
} else { } else {
@@ -323,7 +323,7 @@ export class ReadStateEntry {
const messages = Messages.getMessages(this.channelId); const messages = Messages.getMessages(this.channelId);
const isPrivate = this.isPrivate; const isPrivate = this.isPrivate;
let mentionCount = 0; let mentionCount = 0;
messages.forAll((message) => { messages.forEachBuffered((message) => {
if (snowflakeTimestamp(message.id) <= ackTimestamp) { if (snowflakeTimestamp(message.id) <= ackTimestamp) {
return; return;
} }