Commit 1e2ab8104071
tarball6 files changed+79-5
@@ -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: state124
+ .recent_history125
+ .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(@@ -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 { .. }@@ -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 { .. }@@ -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}@@ -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;@@ -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;