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