6c2f8e0d8e
CI / Build & Quality Checks (push) Failing after 5m48s
P3-1: Message Bookmarks — right-click any message to bookmark; saved to io.lotus.bookmarks account data (max 500, syncs across devices); star icon in sidebar opens BookmarksPanel with filter, Jump-to-message, and remove buttons; reactive to AccountData events P3-2: Message Scheduling (MSC4140) — clock button next to send opens ScheduleMessageModal with datetime-local picker; validates ≥1 min future; calls PUT org.matrix.msc4140 delayed event API; collapsible ScheduledMessagesTray above composer lists pending messages with cancel; local Jotai atom tracks scheduled messages per room P3-3: File Upload Compression — opt-in checkbox per JPEG/PNG file ≥200KB in upload preview; canvas API compresses at 0.82 quality; shows before/ after size estimate; compressed blob used in upload when checked P3-7: Room Insights — new Insights tab in room settings; top 5 active members (bar chart), top 5 reactions (chips), media breakdown (4 tiles), 24-hour activity heatmap (CSS bar chart); all from local cache only with disclaimer banner; never the first tab shown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { MatrixClient } from 'matrix-js-sdk';
|
|
import { useMatrixClient } from './useMatrixClient';
|
|
import { useAccountDataCallback } from './useAccountDataCallback';
|
|
|
|
export type Bookmark = {
|
|
roomId: string;
|
|
eventId: string;
|
|
savedAt: number;
|
|
previewText: string;
|
|
roomName: string;
|
|
};
|
|
|
|
const BOOKMARKS_KEY = 'io.lotus.bookmarks';
|
|
const MAX_BOOKMARKS = 500;
|
|
|
|
type BookmarksContent = {
|
|
bookmarks: Bookmark[];
|
|
};
|
|
|
|
function readBookmarks(mx: MatrixClient): Bookmark[] {
|
|
return (
|
|
(mx.getAccountData(BOOKMARKS_KEY as any)?.getContent() as BookmarksContent | undefined)
|
|
?.bookmarks ?? []
|
|
);
|
|
}
|
|
|
|
export function useBookmarks(): {
|
|
bookmarks: Bookmark[];
|
|
addBookmark: (b: Bookmark) => Promise<void>;
|
|
removeBookmark: (eventId: string) => Promise<void>;
|
|
isBookmarked: (eventId: string) => boolean;
|
|
} {
|
|
const mx = useMatrixClient();
|
|
const [bookmarks, setBookmarks] = useState<Bookmark[]>(() => readBookmarks(mx));
|
|
|
|
useAccountDataCallback(
|
|
mx,
|
|
useCallback(
|
|
(evt) => {
|
|
if (evt.getType() === BOOKMARKS_KEY) {
|
|
setBookmarks(evt.getContent<BookmarksContent>()?.bookmarks ?? []);
|
|
}
|
|
},
|
|
[setBookmarks],
|
|
),
|
|
);
|
|
|
|
// Re-read on mx change
|
|
useEffect(() => {
|
|
setBookmarks(readBookmarks(mx));
|
|
}, [mx]);
|
|
|
|
const addBookmark = useCallback(
|
|
async (b: Bookmark) => {
|
|
const current = readBookmarks(mx);
|
|
// Avoid duplicates
|
|
const filtered = current.filter((bk) => bk.eventId !== b.eventId);
|
|
let next = [b, ...filtered];
|
|
if (next.length > MAX_BOOKMARKS) {
|
|
next = next.slice(0, MAX_BOOKMARKS);
|
|
}
|
|
await (mx as any).setAccountData(BOOKMARKS_KEY, { bookmarks: next });
|
|
},
|
|
[mx],
|
|
);
|
|
|
|
const removeBookmark = useCallback(
|
|
async (eventId: string) => {
|
|
const current = readBookmarks(mx);
|
|
const next = current.filter((bk) => bk.eventId !== eventId);
|
|
await (mx as any).setAccountData(BOOKMARKS_KEY, { bookmarks: next });
|
|
},
|
|
[mx],
|
|
);
|
|
|
|
const isBookmarked = useCallback(
|
|
(eventId: string) => bookmarks.some((bk) => bk.eventId === eventId),
|
|
[bookmarks],
|
|
);
|
|
|
|
return { bookmarks, addBookmark, removeBookmark, isBookmarked };
|
|
}
|