fix(frontend): jumps to replies, pins and album photos load the message when it is not on screen

This commit is contained in:
hh committed 2026-10-19 03:00:12 +00:00
1 parent 9f16e2aa56
commit b9a0604426
1 file changed
+125 -34
+125 -34
View File
@@ -12,6 +12,7 @@
import Icon from "#lib/components/ui/Icon.svelte";
import Spinner from "#lib/components/ui/Spinner.svelte";
import { formatDay } from "#lib/format/datetime.js";
import { t } from "#lib/i18n/locale.svelte.js";
import { accounts } from "#lib/stores/accounts.svelte.js";
import { events } from "#lib/stores/events.svelte.js";
import { peers } from "#lib/stores/peers.svelte.js";
@@ -117,16 +118,78 @@
peers.ensure(ids);
}
function memberIds(message: MessageView): number[] {
return [message.message_id, ...message.media.map((ref) => ref.message_id)];
}
function holds(message: MessageView, messageId: number): boolean {
return memberIds(message).includes(messageId);
}
function sameAlbum(a: MessageView, b: MessageView): boolean {
return (
a.media_group_id !== null &&
a.media_group_id === b.media_group_id &&
a.chat_id === b.chat_id
);
}
function albumPrimary(base: MessageView, incoming: MessageView) {
if (
base.message_id === incoming.message_id ||
(incoming.text && !base.text)
) {
return incoming;
}
if (base.text) {
return base;
}
return incoming.message_id < base.message_id ? incoming : base;
}
function joinAlbum(base: MessageView, incoming: MessageView): MessageView {
const media = new Map(base.media.map((ref) => [ref.message_id, ref]));
for (const ref of incoming.media) {
media.set(ref.message_id, ref);
}
return {
...albumPrimary(base, incoming),
media: [...media.values()].sort((a, b) => a.message_id - b.message_id),
};
}
function mergeAlbums(list: MessageView[]): MessageView[] {
const out: MessageView[] = [];
for (const message of list) {
const prev = out.at(-1);
if (prev && sameAlbum(prev, message)) {
out[out.length - 1] = joinAlbum(prev, message);
} else {
out.push(message);
}
}
return out;
}
function knownIds(): Set<number> {
return new Set(messages.flatMap(memberIds));
}
function ownerOf(messageId: number): number | null {
return messages.find((m) => holds(m, messageId))?.message_id ?? null;
}
function jumpToMessage(messageId: number) {
const owner = ownerOf(messageId) ?? messageId;
const target = container?.querySelector<HTMLElement>(
`[data-message-id="${messageId}"]`
`[data-message-id="${owner}"]`
);
if (!target) {
toasts.error("Message not loaded");
toasts.error(t("thread.notFound"));
return;
}
target.scrollIntoView({ behavior: "smooth", block: "center" });
highlightId = messageId;
highlightId = owner;
if (highlightTimer) {
clearTimeout(highlightTimer);
}
@@ -160,20 +223,28 @@
loading = true;
hasMore = true;
hasNewer = false;
const pending = ui.jumpTarget?.chatId === chatId ? ui.jumpTarget : null;
try {
if (pending) {
ui.clearJump();
await loadAround(pending.messageId);
await tick();
jumpToMessage(pending.messageId);
return;
}
const page = await listMessages(chatId, {
include_deleted: true,
limit: PAGE,
offset: 0,
});
messages = [...page].reverse();
messages = mergeAlbums([...page].reverse());
hasMore = page.length > 0;
ensurePeers(messages);
await tick();
scrollToBottom();
updateStuck();
} catch {
toasts.error("Failed to load messages");
toasts.error(t("thread.loadFailed"));
} finally {
loading = false;
}
@@ -215,16 +286,18 @@
const prevTop = el.scrollTop;
try {
const page = await listMessages(chatId, {
before_id: messages[0].message_id,
before_id: Math.min(...memberIds(messages[0])),
include_deleted: true,
limit: PAGE,
});
const known = new Set(messages.map((m) => m.message_id));
const fresh = page.filter((m) => !known.has(m.message_id));
const known = knownIds();
const fresh = page.filter((m) =>
memberIds(m).some((id) => !known.has(id))
);
hasMore = fresh.length > 0;
if (fresh.length > 0) {
suppressAppear = true;
messages = [...[...fresh].reverse(), ...messages];
messages = mergeAlbums([...[...fresh].reverse(), ...messages]);
ensurePeers(fresh);
await tick();
el.scrollTop = prevTop + (el.scrollHeight - prevHeight);
@@ -242,17 +315,20 @@
}
loadingNewer = true;
try {
const last = messages.at(-1);
const page = await listMessages(chatId, {
after_id: messages.at(-1)?.message_id,
after_id: last ? Math.max(...memberIds(last)) : undefined,
include_deleted: true,
limit: PAGE,
});
const known = new Set(messages.map((m) => m.message_id));
const fresh = page.filter((m) => !known.has(m.message_id));
const known = knownIds();
const fresh = page.filter((m) =>
memberIds(m).some((id) => !known.has(id))
);
hasNewer = fresh.length > 0;
if (fresh.length > 0) {
suppressAppear = true;
messages = [...messages, ...fresh];
messages = mergeAlbums([...messages, ...fresh]);
ensurePeers(fresh);
await tick();
await trimHead();
@@ -278,7 +354,7 @@
limit: PAGE,
}),
]);
messages = [...[...older].reverse(), ...newer];
messages = mergeAlbums([...[...older].reverse(), ...newer]);
hasMore = older.length > 0;
hasNewer = newer.length > 0;
ensurePeers(messages);
@@ -331,13 +407,20 @@
}
function replaceMessage(message: MessageView) {
messages = messages.map((m) =>
m.message_id === message.message_id ? message : m
);
messages = messages.map((m) => {
if (sameAlbum(m, message)) {
return joinAlbum(m, message);
}
return m.message_id === message.message_id ? message : m;
});
}
async function appendMessage(message: MessageView) {
if (messages.some((m) => m.message_id === message.message_id)) {
const last = messages.at(-1);
if (
messages.some((m) => holds(m, message.message_id)) ||
(last && sameAlbum(last, message))
) {
replaceMessage(message);
return;
}
@@ -357,7 +440,7 @@
function applyDelete(ids: number[]) {
const now = new Date().toISOString();
messages = messages.map((m) =>
ids.includes(m.message_id) && m.deleted_at === null
m.deleted_at === null && ids.some((id) => holds(m, id))
? { ...m, deleted_at: now }
: m
);
@@ -403,20 +486,24 @@
});
const fresh = [...page].reverse();
const known = new Map(messages.map((m) => [m.message_id, m]));
const maxId = messages.reduce((acc, m) => Math.max(acc, m.message_id), 0);
const maxId = Math.max(0, ...messages.flatMap(memberIds));
const stick = isNearBottom();
const appended: MessageView[] = [];
for (const message of fresh) {
if (known.has(message.message_id)) {
known.set(message.message_id, message);
const current = known.get(message.message_id);
if (current) {
known.set(
message.message_id,
sameAlbum(current, message) ? joinAlbum(current, message) : message
);
} else if (message.message_id > maxId) {
appended.push(message);
}
}
messages = [
messages = mergeAlbums([
...messages.map((m) => known.get(m.message_id) ?? m),
...appended,
];
]);
ensurePeers(fresh);
await trimHead();
if (appended.length > 0 && stick) {
@@ -425,12 +512,16 @@
}
}
async function jumpToTarget(messageId: number) {
if (!messages.some((m) => m.message_id === messageId)) {
await loadAround(messageId);
async function goTo(messageId: number) {
try {
if (ownerOf(messageId) === null) {
await loadAround(messageId);
}
await tick();
jumpToMessage(messageId);
} catch {
toasts.error(t("thread.loadFailed"));
}
await tick();
jumpToMessage(messageId);
}
$effect(() => {
@@ -439,7 +530,7 @@
return;
}
ui.clearJump();
jumpToTarget(target.messageId);
goTo(target.messageId);
});
$effect(() => {
@@ -460,7 +551,7 @@
</script>
<div class="message-pane">
<PinnedBar {chatId} onjump={jumpToMessage} />
<PinnedBar {chatId} onjump={goTo} />
<div
class="message-list custom-scroll"
onscroll={onScroll}
@@ -490,7 +581,7 @@
</div>
{/if}
{#if row.message.service}
<ActionMessage message={row.message} onjump={jumpToMessage} />
<ActionMessage message={row.message} onjump={goTo} />
{:else}
<MessageBubble
animate={!suppressAppear}
@@ -499,7 +590,7 @@
{isGroupChat}
lastInGroup={row.lastInGroup}
message={row.message}
onjump={jumpToMessage}
onjump={goTo}
onmedia={(index) => openMedia(row.message, index)}
onversions={() => openVersions(row.message.message_id)}
own={row.own}
@@ -514,7 +605,7 @@
</div>
{#if hasNewer}
<button
aria-label="К последним сообщениям"
aria-label={t("thread.toLatest")}
class="to-latest"
onclick={goToLatest}
type="button"