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