feat: add annotations, user profiles, watchers, stories, search and more
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import MessageVersions from "$lib/components/MessageVersions.svelte";
|
||||
import PinnedBar from "$lib/components/PinnedBar.svelte";
|
||||
import EmptyState from "$lib/components/ui/EmptyState.svelte";
|
||||
import Icon from "$lib/components/ui/Icon.svelte";
|
||||
import Spinner from "$lib/components/ui/Spinner.svelte";
|
||||
import { formatDay } from "$lib/format/datetime";
|
||||
import { accounts } from "$lib/stores/accounts.svelte";
|
||||
@@ -30,12 +31,13 @@
|
||||
const SCROLL_THRESHOLD = 160;
|
||||
const STICK_OFFSET = 9;
|
||||
const IDLE_DELAY = 1500;
|
||||
const JUMP_MAX_PAGES = 12;
|
||||
|
||||
let messages = $state<MessageView[]>([]);
|
||||
let loading = $state(true);
|
||||
let loadingOlder = $state(false);
|
||||
let loadingNewer = $state(false);
|
||||
let hasMore = $state(true);
|
||||
let hasNewer = $state(false);
|
||||
let container = $state<HTMLDivElement | null>(null);
|
||||
let suppressAppear = $state(false);
|
||||
let scrolling = $state(false);
|
||||
@@ -155,6 +157,7 @@
|
||||
async function loadInitial() {
|
||||
loading = true;
|
||||
hasMore = true;
|
||||
hasNewer = false;
|
||||
try {
|
||||
const page = await listMessages(chatId, {
|
||||
limit: PAGE,
|
||||
@@ -162,7 +165,7 @@
|
||||
include_deleted: true,
|
||||
});
|
||||
messages = [...page].reverse();
|
||||
hasMore = page.length === PAGE;
|
||||
hasMore = page.length > 0;
|
||||
ensurePeers(messages);
|
||||
await tick();
|
||||
scrollToBottom();
|
||||
@@ -175,33 +178,95 @@
|
||||
}
|
||||
|
||||
async function loadOlder() {
|
||||
if (loadingOlder || !hasMore || container === null) {
|
||||
if (
|
||||
loadingOlder ||
|
||||
!hasMore ||
|
||||
container === null ||
|
||||
messages.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
loadingOlder = true;
|
||||
suppressAppear = true;
|
||||
const el = container;
|
||||
const prevHeight = el.scrollHeight;
|
||||
const prevTop = el.scrollTop;
|
||||
try {
|
||||
const page = await listMessages(chatId, {
|
||||
limit: PAGE,
|
||||
offset: messages.length,
|
||||
before_id: messages[0].message_id,
|
||||
include_deleted: true,
|
||||
});
|
||||
if (page.length > 0) {
|
||||
messages = [...[...page].reverse(), ...messages];
|
||||
ensurePeers(page);
|
||||
const known = new Set(messages.map((m) => m.message_id));
|
||||
const fresh = page.filter((m) => !known.has(m.message_id));
|
||||
hasMore = fresh.length > 0;
|
||||
if (fresh.length > 0) {
|
||||
suppressAppear = true;
|
||||
messages = [...[...fresh].reverse(), ...messages];
|
||||
ensurePeers(fresh);
|
||||
await tick();
|
||||
el.scrollTop = prevTop + (el.scrollHeight - prevHeight);
|
||||
}
|
||||
hasMore = page.length === PAGE;
|
||||
await tick();
|
||||
el.scrollTop = prevTop + (el.scrollHeight - prevHeight);
|
||||
} finally {
|
||||
loadingOlder = false;
|
||||
suppressAppear = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNewer() {
|
||||
if (loadingNewer || !hasNewer || messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
loadingNewer = true;
|
||||
try {
|
||||
const page = await listMessages(chatId, {
|
||||
limit: PAGE,
|
||||
after_id: messages.at(-1)?.message_id,
|
||||
include_deleted: true,
|
||||
});
|
||||
const known = new Set(messages.map((m) => m.message_id));
|
||||
const fresh = page.filter((m) => !known.has(m.message_id));
|
||||
hasNewer = fresh.length > 0;
|
||||
if (fresh.length > 0) {
|
||||
suppressAppear = true;
|
||||
messages = [...messages, ...fresh];
|
||||
ensurePeers(fresh);
|
||||
await tick();
|
||||
}
|
||||
} finally {
|
||||
loadingNewer = false;
|
||||
suppressAppear = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAround(messageId: number) {
|
||||
suppressAppear = true;
|
||||
try {
|
||||
const [older, newer] = await Promise.all([
|
||||
listMessages(chatId, {
|
||||
limit: PAGE,
|
||||
before_id: messageId + 1,
|
||||
include_deleted: true,
|
||||
}),
|
||||
listMessages(chatId, {
|
||||
limit: PAGE,
|
||||
after_id: messageId,
|
||||
include_deleted: true,
|
||||
}),
|
||||
]);
|
||||
messages = [...[...older].reverse(), ...newer];
|
||||
hasMore = older.length > 0;
|
||||
hasNewer = newer.length > 0;
|
||||
ensurePeers(messages);
|
||||
await tick();
|
||||
} finally {
|
||||
suppressAppear = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function goToLatest() {
|
||||
await loadInitial();
|
||||
}
|
||||
|
||||
function onScroll() {
|
||||
scrolling = true;
|
||||
if (idleTimer) {
|
||||
@@ -214,6 +279,9 @@
|
||||
if (container && container.scrollTop < SCROLL_THRESHOLD) {
|
||||
loadOlder();
|
||||
}
|
||||
if (hasNewer && isNearBottom()) {
|
||||
loadNewer();
|
||||
}
|
||||
}
|
||||
|
||||
function openMedia(message: MessageView, index: number) {
|
||||
@@ -248,6 +316,9 @@
|
||||
replaceMessage(message);
|
||||
return;
|
||||
}
|
||||
if (hasNewer) {
|
||||
return;
|
||||
}
|
||||
const stick = isNearBottom();
|
||||
messages = [...messages, message];
|
||||
ensurePeers([message]);
|
||||
@@ -296,7 +367,7 @@
|
||||
}
|
||||
|
||||
async function resyncTail() {
|
||||
if (messages.length === 0) {
|
||||
if (messages.length === 0 || hasNewer) {
|
||||
return;
|
||||
}
|
||||
const page = await listMessages(chatId, {
|
||||
@@ -328,15 +399,8 @@
|
||||
}
|
||||
|
||||
async function jumpToTarget(messageId: number) {
|
||||
let guard = 0;
|
||||
while (
|
||||
!messages.some((m) => m.message_id === messageId) &&
|
||||
hasMore &&
|
||||
(messages.length === 0 || messages[0].message_id > messageId) &&
|
||||
guard < JUMP_MAX_PAGES
|
||||
) {
|
||||
await loadOlder();
|
||||
guard++;
|
||||
if (!messages.some((m) => m.message_id === messageId)) {
|
||||
await loadAround(messageId);
|
||||
}
|
||||
await tick();
|
||||
jumpToMessage(messageId);
|
||||
@@ -422,9 +486,22 @@
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if loadingNewer}
|
||||
<div class="loading-older"><Spinner /></div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if hasNewer}
|
||||
<button
|
||||
type="button"
|
||||
class="to-latest"
|
||||
onclick={goToLatest}
|
||||
aria-label="К последним сообщениям"
|
||||
>
|
||||
<Icon name="arrow-down" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<MediaViewer
|
||||
@@ -441,11 +518,37 @@
|
||||
|
||||
<style lang="scss">
|
||||
.message-pane {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.to-latest {
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
bottom: 1.25rem;
|
||||
z-index: var(--z-sticky-date);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 16%);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.message-list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user