feat(frontend): feed of deleted and edited messages above the chat list and per chat in the changes panel

This commit is contained in:
hh committed 2026-10-19 03:00:12 +00:00
1 parent 010fa29ce0
commit 9f16e2aa56
23 files changed
+1915 -71

No files matched your search

+5
View File
@@ -79,3 +79,8 @@ export function timeZoneLabel(): string {
.find((part) => part.type === "timeZoneName")?.value ?? browserTimeZone()
);
}
export function formatDayTime(iso: string): string {
const date = new Date(iso);
return `${format("day", date)}, ${format("time", date)}`;
}
+74
View File
@@ -0,0 +1,74 @@
export interface DiffPart {
changed: boolean;
text: string;
}
export interface WordDiff {
after: DiffPart[];
before: DiffPart[];
}
const TOKEN = /(\s+)/;
const MAX_TOKENS = 400;
function tokens(text: string): string[] {
return text.split(TOKEN).filter(Boolean);
}
function push(parts: DiffPart[], text: string, changed: boolean) {
const last = parts.at(-1);
if (last && last.changed === changed) {
last.text += text;
} else {
parts.push({ changed, text });
}
}
function lcsTable(a: string[], b: string[]): Uint16Array {
const width = b.length + 1;
const table = new Uint16Array((a.length + 1) * width);
for (let i = a.length - 1; i >= 0; i -= 1) {
for (let j = b.length - 1; j >= 0; j -= 1) {
table[i * width + j] =
a[i] === b[j]
? table[(i + 1) * width + j + 1] + 1
: Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
}
}
return table;
}
export function wordDiff(before: string, after: string): WordDiff | null {
const a = tokens(before);
const b = tokens(after);
if (a.length > MAX_TOKENS || b.length > MAX_TOKENS) {
return null;
}
const table = lcsTable(a, b);
const width = b.length + 1;
const left: DiffPart[] = [];
const right: DiffPart[] = [];
let i = 0;
let j = 0;
while (i < a.length && j < b.length) {
if (a[i] === b[j]) {
push(left, a[i], false);
push(right, b[j], false);
i += 1;
j += 1;
} else if (table[(i + 1) * width + j] >= table[i * width + j + 1]) {
push(left, a[i], true);
i += 1;
} else {
push(right, b[j], true);
j += 1;
}
}
for (const token of a.slice(i)) {
push(left, token, true);
}
for (const token of b.slice(j)) {
push(right, token, true);
}
return { after: right, before: left };
}
+21 -18
View File
@@ -1,27 +1,30 @@
const MEDIA_KIND_LABELS: Record<string, string> = {
animation: "GIF",
audio: "Аудио",
contact: "Контакт",
dice: "Кубик",
document: "Файл",
game: "Игра",
gif: "GIF",
location: "Геопозиция",
photo: "Фото",
poll: "Опрос",
sticker: "Стикер",
story: "Сторис",
venue: "Геопозиция",
video: "Видео",
video_note: "Кружок",
voice: "Голосовое",
import { t } from "#lib/i18n/locale.svelte.js";
import type { MessageKey } from "#lib/i18n/messages.js";
const MEDIA_KIND_KEYS: Record<string, MessageKey> = {
animation: "media.kind.gif",
audio: "media.kind.audio",
contact: "media.kind.contact",
dice: "media.kind.dice",
document: "media.kind.document",
game: "media.kind.game",
gif: "media.kind.gif",
location: "media.kind.location",
photo: "media.kind.photo",
poll: "media.kind.poll",
sticker: "media.kind.sticker",
story: "media.kind.story",
venue: "media.kind.location",
video: "media.kind.video",
video_note: "media.kind.videoNote",
voice: "media.kind.voice",
};
export function mediaKindLabel(kind: string | null): string | null {
if (!kind) {
return null;
}
return MEDIA_KIND_LABELS[kind] ?? "Медиа";
return t(MEDIA_KIND_KEYS[kind] ?? "media.kind.other");
}
const BYTE_UNITS = ["B", "KB", "MB", "GB"];