fix(frontend): jumps to replies, pins and album photos load the message when it is not on screen
This commit is contained in:
1 parent
9f16e2aa56
commit
b9a0604426
1 file changed
+125
-34
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user