Files
cinny/src/app/hooks/useBookmarks.ts
T
jared 6c2f8e0d8e
CI / Build & Quality Checks (push) Failing after 5m48s
feat: bookmarks, message scheduling, image compression, room insights
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>
2026-06-04 10:26:08 -04:00

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 };
}