feat: bookmarks, message scheduling, image compression, room insights
CI / Build & Quality Checks (push) Failing after 5m48s
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>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user