From c6760b0ba4f8c423b307de63c3b38f196829e2eb Mon Sep 17 00:00:00 2001 From: Jared Vititoe Date: Thu, 4 Jun 2026 12:07:12 -0400 Subject: [PATCH] fix: schedule button, compression visibility, activity log, insights overflow, bookmarks UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedule message: modal now always opens (even with empty composer); includes its own message textarea pre-filled from editor content; removed null-content early-return guard from handleScheduleClick; fixed error text to use color.Critical.Main not CSS var Image compression: removed 200KB size threshold — checkbox now shows for all JPEG/PNG uploads (not just large ones); 'no significant saving' message handles already-small files gracefully Activity log: auto-paginate on mount — state events are absent from initial sync window, so the log was always empty until Load More was clicked manually Insights summary: Text size H4→H5 with toLocaleString() formatting and overflow:ellipsis so large numbers don't push tiles off screen Bookmarks panel: replaced var(--bg-*) CSS vars (undefined in folds themes) with color.Surface/SurfaceVariant/Primary folds tokens; added left accent border on message preview block, count badge, clear button in search, improved empty state, cleaner button hierarchy Co-Authored-By: Claude Sonnet 4.6 --- src/app/features/bookmarks/BookmarksPanel.tsx | 175 ++++++++++++------ .../room-settings/RoomActivityLog.tsx | 7 + .../features/room-settings/RoomInsights.tsx | 39 +++- src/app/features/room/RoomInput.tsx | 8 +- .../features/room/ScheduleMessageModal.tsx | 59 +++--- src/app/utils/imageCompression.ts | 5 +- 6 files changed, 202 insertions(+), 91 deletions(-) diff --git a/src/app/features/bookmarks/BookmarksPanel.tsx b/src/app/features/bookmarks/BookmarksPanel.tsx index ce5ead1ea..bf2b9229a 100644 --- a/src/app/features/bookmarks/BookmarksPanel.tsx +++ b/src/app/features/bookmarks/BookmarksPanel.tsx @@ -1,5 +1,17 @@ import React, { ChangeEvent, useState } from 'react'; -import { Box, Button, Header, Icon, IconButton, Icons, Input, Scroll, Text, config } from 'folds'; +import { + Box, + Button, + Header, + Icon, + IconButton, + Icons, + Input, + Scroll, + Text, + color, + config, +} from 'folds'; import { useBookmarks, Bookmark } from '../../hooks/useBookmarks'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; @@ -14,8 +26,6 @@ function formatTimeAgo(ts: number): string { const days = Math.floor(hours / 24); if (days === 1) return 'yesterday'; if (days < 7) return `${days}d ago`; - const weeks = Math.floor(days / 7); - if (weeks < 5) return `${weeks}w ago`; return new Date(ts).toLocaleDateString(); } @@ -30,65 +40,83 @@ function BookmarkItem({ bookmark, onJump, onRemove }: BookmarkItemProps) { const room = mx.getRoom(bookmark.roomId); const displayRoomName = room?.name ?? bookmark.roomName; - const handleJump = () => { - onJump(bookmark.roomId, bookmark.eventId); - }; - - const handleRemove = () => { - onRemove(bookmark.eventId); - }; - return ( - - {displayRoomName} - - + + + {displayRoomName} + + + + {/* Message preview */} + - {bookmark.previewText || '(no preview)'} - + + {bookmark.previewText || '(no preview)'} + + + + {/* Footer row */} - - Saved {formatTimeAgo(bookmark.savedAt)} + + {formatTimeAgo(bookmark.savedAt)} onRemove(bookmark.eventId)} aria-label="Remove bookmark" > - + @@ -129,22 +157,21 @@ export function BookmarksPanel({ onClose }: BookmarksPanelProps) { width: '266px', height: '100%', flexShrink: 0, - borderLeftWidth: config.borderWidth.B300, - borderLeftStyle: 'solid', - borderLeftColor: 'var(--bg-surface-border)', - backgroundColor: 'var(--bg-background)', + borderLeft: `1px solid ${color.Surface.ContainerLine}`, + background: color.Surface.Container, overflow: 'hidden', display: 'flex', flexDirection: 'column', }} > + {/* Header */}
@@ -152,44 +179,84 @@ export function BookmarksPanel({ onClose }: BookmarksPanelProps) { Saved Messages - - + +
+ {/* Search */} } + after={ + filter.length > 0 ? ( + setFilter('')} + > + + + ) : undefined + } /> + {/* Count badge */} + {bookmarks.length > 0 && ( + + + {filtered.length === bookmarks.length + ? `${bookmarks.length} saved message${bookmarks.length !== 1 ? 's' : ''}` + : `${filtered.length} of ${bookmarks.length} messages`} + + + )} + + {/* List */} {filtered.length === 0 ? ( - + {bookmarks.length === 0 - ? 'No saved messages yet. Right-click any message to bookmark it.' - : 'No bookmarks match your filter.'} + ? 'No saved messages yet.\nRight-click any message to bookmark it.' + : 'No bookmarks match your search.'} ) : ( diff --git a/src/app/features/room-settings/RoomActivityLog.tsx b/src/app/features/room-settings/RoomActivityLog.tsx index 70a342777..73bb5e361 100644 --- a/src/app/features/room-settings/RoomActivityLog.tsx +++ b/src/app/features/room-settings/RoomActivityLog.tsx @@ -316,6 +316,13 @@ export function RoomActivityLog({ requestClose }: RoomActivityLogProps) { setEvents(getStateEvents()); }, [getStateEvents]); + // Auto-paginate on mount — state events are rarely in the initial sync + // window, so we immediately fetch backwards to populate the log. + useEffect(() => { + handleLoadMore(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const handleLoadMore = useCallback(async () => { if (loading || !canLoadMore) return; setLoading(true); diff --git a/src/app/features/room-settings/RoomInsights.tsx b/src/app/features/room-settings/RoomInsights.tsx index 7da71bb9d..7b616923f 100644 --- a/src/app/features/room-settings/RoomInsights.tsx +++ b/src/app/features/room-settings/RoomInsights.tsx @@ -208,8 +208,17 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { background: color.Surface.Container, }} > - - {stats.totalMessages} + + {stats.totalMessages.toLocaleString()} Messages @@ -228,8 +237,17 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { background: color.Surface.Container, }} > - - {stats.uniqueParticipants} + + {stats.uniqueParticipants.toLocaleString()} Participants @@ -248,8 +266,17 @@ export function RoomInsights({ requestClose }: RoomInsightsProps) { background: color.Surface.Container, }} > - - {stats.totalCached} + + {stats.totalCached.toLocaleString()} Cached events diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index 429cdd972..324fa1843 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -598,8 +598,8 @@ export const RoomInput = forwardRef( }, [editor, isMarkdown, mx, roomId, replyDraft]); const handleScheduleClick = useCallback(() => { + // Pre-fill from editor if there's content; open blank if editor is empty. const content = buildCurrentTextContent(); - if (!content) return; setScheduleContent(content); setScheduleOpen(true); }, [buildCurrentTextContent]); @@ -1171,10 +1171,12 @@ export const RoomInput = forwardRef( } /> {pollOpen && setPollOpen(false)} />} - {scheduleOpen && scheduleContent && ( + {scheduleOpen && ( { setScheduleOpen(false); diff --git a/src/app/features/room/ScheduleMessageModal.tsx b/src/app/features/room/ScheduleMessageModal.tsx index 5fcfbffe9..d2f179b5f 100644 --- a/src/app/features/room/ScheduleMessageModal.tsx +++ b/src/app/features/room/ScheduleMessageModal.tsx @@ -22,7 +22,8 @@ import { scheduleMessage } from '../../utils/scheduledMessages'; interface ScheduleMessageModalProps { roomId: string; - content: IContent; + /** Pre-fill the message body from the composer. Pass null/undefined to open blank. */ + initialBody?: string; onScheduled: (delayId: string, sendAt: number, content: IContent) => void; onClose: () => void; } @@ -66,11 +67,12 @@ function toLocalDatetimeValue(date: Date): string { export function ScheduleMessageModal({ roomId, - content, + initialBody, onScheduled, onClose, }: ScheduleMessageModalProps) { const mx = useMatrixClient(); + const [messageText, setMessageText] = useState(initialBody ?? ''); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -126,6 +128,12 @@ export function ScheduleMessageModal({ return; } + if (!messageText.trim()) { + setError('Please enter a message to schedule.'); + return; + } + + const content: IContent = { body: messageText.trim(), msgtype: 'm.text' }; setError(null); setSubmitting(true); try { @@ -187,31 +195,32 @@ export function ScheduleMessageModal({ {/* Body */} - {/* Message preview */} - {typeof content.body === 'string' && content.body.trim() !== '' && ( - + + Message + +