fix: schedule button, compression visibility, activity log, insights overflow, bookmarks UI

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 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 12:07:12 -04:00
parent 8f5afcda08
commit c6760b0ba4
6 changed files with 202 additions and 91 deletions
+121 -54
View File
@@ -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 (
<Box
direction="Column"
gap="100"
gap="200"
style={{
padding: `${config.space.S200} ${config.space.S300}`,
borderBottomWidth: config.borderWidth.B300,
borderBottomStyle: 'solid',
borderBottomColor: 'var(--bg-surface-border)',
padding: `${config.space.S300} ${config.space.S300}`,
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
background: color.Surface.Container,
transition: 'background 0.1s',
}}
>
<Text size="L400" truncate>
{displayRoomName}
</Text>
<Text
size="T300"
priority="300"
{/* Room name row */}
<Box alignItems="Center" gap="100">
<Icon src={Icons.Hash} size="50" style={{ opacity: 0.5, flexShrink: 0 }} />
<Text
size="T200"
style={{
color: color.Primary.Main,
fontWeight: 600,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{displayRoomName}
</Text>
</Box>
{/* Message preview */}
<Box
style={{
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
wordBreak: 'break-word',
background: color.SurfaceVariant.Container,
borderRadius: config.radii.R300,
borderLeft: `3px solid ${color.Primary.Main}`,
padding: `${config.space.S100} ${config.space.S200}`,
}}
>
{bookmark.previewText || '(no preview)'}
</Text>
<Text
size="T300"
style={{
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
wordBreak: 'break-word',
opacity: 0.9,
}}
>
{bookmark.previewText || '(no preview)'}
</Text>
</Box>
{/* Footer row */}
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="T200" priority="300">
Saved {formatTimeAgo(bookmark.savedAt)}
<Text size="T200" style={{ opacity: 0.5 }}>
{formatTimeAgo(bookmark.savedAt)}
</Text>
<Box gap="100" shrink="No">
<Button
size="300"
variant="Secondary"
variant="Primary"
fill="Soft"
radii="300"
onClick={handleJump}
onClick={() => onJump(bookmark.roomId, bookmark.eventId)}
before={<Icon size="100" src={Icons.ArrowRight} />}
>
<Text size="T300">Jump</Text>
</Button>
<IconButton
size="300"
variant="Critical"
variant="Surface"
fill="None"
radii="300"
onClick={handleRemove}
onClick={() => onRemove(bookmark.eventId)}
aria-label="Remove bookmark"
>
<Icon size="100" src={Icons.Cross} />
<Icon size="100" src={Icons.Delete} />
</IconButton>
</Box>
</Box>
@@ -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 */}
<Header
style={{
flexShrink: 0,
padding: `0 ${config.space.S200} 0 ${config.space.S300}`,
borderBottomWidth: config.borderWidth.B300,
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
}}
variant="Background"
variant="Surface"
size="600"
>
<Box grow="Yes" alignItems="Center" gap="200">
@@ -152,44 +179,84 @@ export function BookmarksPanel({ onClose }: BookmarksPanelProps) {
<Box grow="Yes">
<Text size="H5">Saved Messages</Text>
</Box>
<IconButton variant="Background" aria-label="Close saved messages" onClick={onClose}>
<Icon src={Icons.Cross} />
<IconButton
size="300"
variant="Surface"
radii="300"
aria-label="Close saved messages"
onClick={onClose}
>
<Icon src={Icons.Cross} size="100" />
</IconButton>
</Box>
</Header>
{/* Search */}
<Box
style={{
flexShrink: 0,
padding: config.space.S200,
borderBottomWidth: config.borderWidth.B300,
borderBottomStyle: 'solid',
borderBottomColor: 'var(--bg-surface-border)',
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
background: color.SurfaceVariant.Container,
}}
>
<Input
variant="Background"
placeholder="Filter saved messages..."
variant="Surface"
size="400"
radii="400"
placeholder="Search saved messages…"
value={filter}
onChange={handleFilterChange}
before={<Icon size="200" src={Icons.Search} />}
after={
filter.length > 0 ? (
<IconButton
size="300"
variant="Surface"
radii="300"
aria-label="Clear search"
onClick={() => setFilter('')}
>
<Icon size="100" src={Icons.Cross} />
</IconButton>
) : undefined
}
/>
</Box>
{/* Count badge */}
{bookmarks.length > 0 && (
<Box
style={{
flexShrink: 0,
padding: `${config.space.S100} ${config.space.S300}`,
borderBottom: `1px solid ${color.Surface.ContainerLine}`,
background: color.SurfaceVariant.Container,
}}
>
<Text size="T200" style={{ opacity: 0.6 }}>
{filtered.length === bookmarks.length
? `${bookmarks.length} saved message${bookmarks.length !== 1 ? 's' : ''}`
: `${filtered.length} of ${bookmarks.length} messages`}
</Text>
</Box>
)}
{/* List */}
<Scroll variant="Background" size="300" style={{ flexGrow: 1, minHeight: 0 }}>
{filtered.length === 0 ? (
<Box
direction="Column"
alignItems="Center"
justifyContent="Center"
gap="200"
style={{ padding: config.space.S500 }}
gap="300"
style={{ padding: config.space.S600, textAlign: 'center' }}
>
<Icon size="600" src={Icons.Star} />
<Icon size="600" src={Icons.Star} style={{ opacity: 0.3 }} />
<Text size="T300" priority="300" align="Center">
{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.'}
</Text>
</Box>
) : (