atagen

malice

public
Star0
Watch0

Commit 1e2ab8104071

tarball

atagen <boss@atagen.co>2026-10-25 09:09 UTC

6 files changed+79-5
history: publish recent loading state
Modified file: crates/malice-core/src/feed.rs+16 additions−2 deletions
@@ -3,8 +3,8 @@ use std::collections::HashMap;
33 use futures_util::{Stream, StreamExt, stream};
44 use macltok::{
55 ConnectionState, EncryptionState, ImagePacks, MentionsPage, MentionsUpdate, MirrorHandle,
6- RoomSummary, SessionStatus, State, StateChange, TimelineChange, TimelineId, TimelineItem,
7- UploadOutcome, VerificationState, imbl::Vector,
6+ RecentHistoryState, RoomSummary, SessionStatus, State, StateChange, TimelineChange, TimelineId,
7+ TimelineItem, UploadOutcome, VerificationState, imbl::Vector,
88 };
99 use serde::Serialize;
1010
@@ -20,10 +20,15 @@ pub enum WireEvent {
2020 verification: Option<VerificationState>,
2121 image_packs: ImagePacks,
2222 mentions: Option<MentionsPage>,
23+ recent_history: HashMap<String, RecentHistoryState>,
2324 },
2425 Mentions {
2526 update: MentionsUpdate,
2627 },
28+ RecentHistory {
29+ room_id: String,
30+ state: RecentHistoryState,
31+ },
2732 ImagePacks {
2833 packs: ImagePacks,
2934 },
@@ -115,6 +120,11 @@ pub fn snapshot_event(state: &State) -> WireEvent {
115120 verification: state.verification.clone(),
116121 image_packs: state.image_packs.clone(),
117122 mentions: state.mentions.clone(),
123+ recent_history: state
124+ .recent_history
125+ .iter()
126+ .map(|(room_id, status)| (room_id.as_str().to_owned(), status.clone()))
127+ .collect(),
118128 }
119129 }
120130
@@ -184,6 +194,10 @@ pub fn wire_events(change: &StateChange) -> Vec<WireEvent> {
184194 StateChange::Mentions(update) => vec![WireEvent::Mentions {
185195 update: update.clone(),
186196 }],
197+ StateChange::RecentHistory { room_id, state } => vec![WireEvent::RecentHistory {
198+ room_id: room_id.as_str().to_owned(),
199+ state: state.clone(),
200+ }],
187201 StateChange::Failed(failure) => vec![error(&failure.message)],
188202 StateChange::SessionSaveFailed(_) => {
189203 vec![error(
Modified file: crates/malice-server/src/state.rs+1 additions
@@ -411,6 +411,7 @@ async fn wait_authenticated(mirror: &MirrorHandle) -> Option<String> {
411411 | StateChange::Timeline { .. }
412412 | StateChange::History { .. }
413413 | StateChange::HistoryRepair(_)
414+ | StateChange::RecentHistory { .. }
414415 | StateChange::Typing(_)
415416 | StateChange::Upload { .. }
416417 | StateChange::UploadFinished { .. }
Modified file: crates/malice-worker/src/account.rs+1 additions
@@ -112,6 +112,7 @@ async fn wait_authenticated(mirror: &MirrorHandle) -> Option<String> {
112112 | StateChange::Timeline { .. }
113113 | StateChange::History { .. }
114114 | StateChange::HistoryRepair(_)
115+ | StateChange::RecentHistory { .. }
115116 | StateChange::Typing(_)
116117 | StateChange::Upload { .. }
117118 | StateChange::UploadFinished { .. }
Modified file: web/src/App.svelte+31 additions−3 deletions
@@ -31,6 +31,7 @@
3131 getSession,
3232 getSpaceChildren,
3333 fillHistoryGap,
34+ startRecentHistory,
3435 joinRoom,
3536 leaveRoom,
3637 logout,
@@ -64,8 +65,9 @@
6465 membersLoading,
6566 mentionsSnapshot,
6667 pushBanner,
67- markTimelineOpened,
6868 imagePacks,
69+ markTimelineOpened,
70+ recentHistory,
6971 resetClientState,
7072 rooms,
7173 selectedRoomId,
@@ -127,10 +129,12 @@
127129 let leaveBusy = false;
128130 let paginateBusy = false;
129131 let fillingGaps: Record<string, string> = {};
132+ let recentRetrying: Record<string, boolean> = {};
130133 let historyAccount: string | null | undefined;
131134 $: if ($sessionUser !== historyAccount) {
132135 historyAccount = $sessionUser;
133136 fillingGaps = {};
137+ recentRetrying = {};
134138 }
135139 let reacting = false;
136140 let composer = '';
@@ -194,6 +198,7 @@
194198 wasOnline = online;
195199 }
196200 $: selectedRoom = $rooms.find((r) => r.room_id === $selectedRoomId) ?? null;
201+ $: recentState = $selectedRoomId ? $recentHistory[$selectedRoomId] : undefined;
197202 $: roomPacks = enabledPacks(packsForRoom($imagePacks, $selectedRoomId, $rooms), $disabledPackImages);
198203 $: roomEmotes = customEmotes(roomPacks);
199204 $: usableQuickReactions = enabledQuickReactions($quickReactions, allPacks($imagePacks), $disabledPackImages);
@@ -1358,7 +1363,7 @@
13581363
13591364 async function paginate(): Promise<void> {
13601365 const roomId = $selectedRoomId;
1361- if (!roomId || paginateBusy) return;
1366+ if (!roomId || paginateBusy || $recentHistory[roomId] === 'Loading') return;
13621367 paginateBusy = true;
13631368 try {
13641369 await paginateRoom(roomId);
@@ -1385,6 +1390,19 @@
13851390 }
13861391 }
13871392
1393+ async function retryRecentHistory(roomId: string): Promise<void> {
1394+ if (recentRetrying[roomId]) return;
1395+ recentRetrying = { ...recentRetrying, [roomId]: true };
1396+ try {
1397+ await startRecentHistory(roomId);
1398+ } catch (e) {
1399+ pushBanner(e instanceof Error ? e.message : 'Loading recent history failed');
1400+ } finally {
1401+ const { [roomId]: _done, ...rest } = recentRetrying;
1402+ recentRetrying = rest;
1403+ }
1404+ }
1405+
13881406 async function loadMembers(roomId: string): Promise<void> {
13891407 if (membersFor === roomId && $membersLoading) return;
13901408 membersFor = roomId;
@@ -2454,10 +2472,20 @@
24542472 >
24552473 <div class="timeline-inner" use:pinTimeline>
24562474 <div class="timeline-top">
2475+ {#if recentState === 'Loading'}
2476+ <p class="side-note">Loading recent history…</p>
2477+ {:else if recentState && typeof recentState === 'object'}
2478+ <div class="history-gap">
2479+ <span>Recent history could not be loaded</span>
2480+ <button type="button" class="ghost-btn" disabled={!!recentRetrying[selectedRoom.room_id]} on:click={() => void retryRecentHistory(selectedRoom.room_id)}>
2481+ {recentRetrying[selectedRoom.room_id] ? 'Loading…' : 'Retry'}
2482+ </button>
2483+ </div>
2484+ {/if}
24572485 {#if hasStart}
24582486 <p class="start-note">Beginning of history</p>
24592487 {:else}
2460- <button type="button" class="ghost-btn" disabled={paginateBusy} on:click={() => void paginate()}>
2488+ <button type="button" class="ghost-btn" disabled={paginateBusy || recentState === 'Loading'} on:click={() => void paginate()}>
24612489 {paginateBusy ? 'Loading…' : 'Load older messages'}
24622490 </button>
24632491 {/if}
Modified file: web/src/lib/store.ts+12 additions
@@ -12,6 +12,8 @@ import {
1212 EMPTY_IMAGE_PACKS,
1313 parseEncryptionState,
1414 parseImagePacks,
15+ parseRecentHistoryMap,
16+ parseRecentHistoryState,
1517 parseRoomList,
1618 parseTimelineList,
1719 parseVerificationState
@@ -21,6 +23,7 @@ import type {
2123 EventContext,
2224 ImagePacks,
2325 MemberSummary,
26+ RecentHistoryState,
2427 RoomSummary,
2528 TimelineItem,
2629 VerificationState
@@ -43,6 +46,7 @@ export const rooms = writable<RoomSummary[]>([]);
4346 export const timelines = writable<Record<string, TimelineItem[]>>({});
4447 export const pinnedTimelines = writable<Record<string, TimelineItem[]>>({});
4548 export const pinnedTimelineVersions = writable<Record<string, number>>({});
49+export const recentHistory = writable<Record<string, RecentHistoryState>>({});
4650 export interface EventContextState {
4751 room_id: string;
4852 request_id: string;
@@ -80,6 +84,7 @@ export function resetClientState(): void {
8084 timelines.set({});
8185 pinnedTimelines.set({});
8286 pinnedTimelineVersions.set({});
87+ recentHistory.set({});
8388 eventContexts.set({});
8489 connection.set('connecting');
8590 selectedRoomId.set(null);
@@ -154,6 +159,7 @@ function handlePayload(text: string): void {
154159 const kind = data['type'];
155160 if (kind === 'snapshot') {
156161 if (typeof data['connection'] === 'string') setConnection(data['connection']);
162+ recentHistory.set(parseRecentHistoryMap(data['recent_history']));
157163 const mentions = parseMentionsPage(data['mentions']);
158164 if (mentions) publishMentionsPage(mentions);
159165 setRooms(data['rooms']);
@@ -185,6 +191,12 @@ function handlePayload(text: string): void {
185191 if (update) publishMentionsPage(update.snapshot);
186192 return;
187193 }
194+ if (kind === 'recent_history') {
195+ const roomId = data['room_id'];
196+ const state = parseRecentHistoryState(data['state']);
197+ if (typeof roomId === 'string' && state) recentHistory.update((all) => ({ ...all, [roomId]: state }));
198+ return;
199+ }
188200 if (kind === 'image_packs') {
189201 imagePacks.set(parseImagePacks(data['packs']));
190202 return;
Modified file: web/src/lib/types.ts+18 additions
@@ -656,6 +656,24 @@ export type TimelineItem =
656656 | { kind: 'start' }
657657 | { kind: 'unknown'; label: string };
658658
659+export type RecentHistoryState = 'Idle' | 'Loading' | 'Loaded' | { Failed: string };
660+
661+export function parseRecentHistoryState(raw: unknown): RecentHistoryState | null {
662+ if (raw === 'Idle' || raw === 'Loading' || raw === 'Loaded') return raw;
663+ if (isRecord(raw) && typeof raw['Failed'] === 'string') return { Failed: raw['Failed'] };
664+ return null;
665+}
666+
667+export function parseRecentHistoryMap(raw: unknown): Record<string, RecentHistoryState> {
668+ const out: Record<string, RecentHistoryState> = {};
669+ if (!isRecord(raw)) return out;
670+ for (const [roomId, value] of Object.entries(raw)) {
671+ const state = parseRecentHistoryState(value);
672+ if (state) out[roomId] = state;
673+ }
674+ return out;
675+}
676+
659677 export interface HistoryGap {
660678 id: string;
661679 older_boundary: string | null;