feat: P5-21 mention color, P5-22 font selector, P5-27 notification presets; update docs
CI / Build & Quality Checks (push) Successful in 10m27s
Trigger Desktop Build / trigger (push) Successful in 7s

- P5-21: Custom @mention highlight color picker in Settings → Appearance.
  CSS vars with luminance-computed text color; resets cleanly to theme default.
- P5-22: Font selector (System, Inter, JetBrains Mono, Fira Code) in
  Settings → Appearance. Fira Code added to Google Fonts preload.
- P5-27: Gaming/Work/Sleep preset buttons at top of Settings → Notifications.
  Each atomically applies a group of notification settings.
- AppearanceEffects component in App.tsx applies CSS vars on settings change.
- LOTUS_BUGS.md: mark presence + manifest icon bugs as resolved.
- LOTUS_TODO.md: mark P3-6, P5-21, P5-22, P5-27 as [x].
- LOTUS_FEATURES.md: document all four completed features.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 15:39:35 -04:00
parent 891f2daf99
commit 5469740f4c
9 changed files with 257 additions and 36 deletions
+3 -26
View File
@@ -18,6 +18,8 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
* **Encrypted Search Misses Historic Events**: Fixed in `useLocalMessageSearch.ts`.
* **Presence Updater Base URL Hack**: Fixed in `usePresenceUpdater.ts`.
* **Presence Badge Accessibility**: Fixed in `Presence.tsx` (`aria-label` on badge).
* **Presence Updater Wipes Custom Status**: Fixed in `usePresenceUpdater.ts` (removed `status_msg: ''`).
* **Manifest Main Icon Paths 404**: Fixed in `public/manifest.json` (`./public/android/``./res/android/`). Shortcut icon was already correct.
---
@@ -31,15 +33,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
* **Impact:** In encrypted rooms, the edit history shows ciphertext or "(no text)" for all previous versions.
* **Recommended Fix:** After fetching raw events, check if they are encrypted. Use `mx.decryptEventIfNeeded(event)` for each event in the chunk before rendering.
### 2. Presence Updater Wipes Custom Status
**File:** `src/app/hooks/usePresenceUpdater.ts`
**Status:** **OPEN**
* **Issue:** `setOnline` and `setUnavailable` still send `status_msg: ''`.
* **Impact:** Custom status messages are wiped when the user goes idle/active.
* **Recommended Fix:** Remove `status_msg` from the `setPresence` payload in the updater hook.
### 3. Service Worker Ephemeral Sessions
### 2. Service Worker Ephemeral Sessions
**File:** `src/sw.ts`
**Status:** **OPEN**
@@ -75,21 +69,4 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
* **Impact:** Two identical animations (e.g., Digital Rain) run simultaneously, doubling GPU usage on mobile.
* **Recommended Fix:** When Glassmorphism is active, make the `RoomView` background transparent and rely on the `document.body` background.
### 4. Manifest Shortcut Icon 404
**File:** `public/manifest.json`
**Status:** **OPEN**
* **Issue:** The shortcut icon path is `res/android/...` but the file is copied to `public/android/...`.
* **Recommended Fix:** Change the path to `./public/android/android-chrome-96x96.png` in `manifest.json`.
---
## 🎨 UI/UX Consistency
### 1. Night Light Overlay Coverage
**File:** `src/app/pages/App.tsx`
**Status:** **OPEN**
* **Issue:** Overlay is inside `#root`, bypasses `#portalContainer` (modals/tooltips).
* **Recommended Fix:** Move to end of `document.body`.
+42
View File
@@ -174,6 +174,30 @@ A warm orange overlay rendered over the entire UI to reduce blue light emission.
---
## Font Selector (P5-22)
Users can choose the UI font in **Settings → Appearance**:
- **System Default** — `system-ui, -apple-system, sans-serif`
- **Inter** — `'InterVariable', sans-serif` (current default)
- **JetBrains Mono** — `'JetBrains Mono', monospace` (already loaded from Google Fonts)
- **Fira Code** — `'Fira Code', monospace` (added to Google Fonts preload in `index.html`)
Applied by overriding `--font-secondary` on `document.body` via `AppearanceEffects` in `App.tsx`. The TDS terminal mode font stack is unaffected.
---
## Custom @Mention Highlight Color (P5-21)
Users can set a custom background color for `@mention` chips that highlight their own name, in **Settings → Appearance**.
- Color picker (native `<input type="color">`) with a **Reset** button to revert to the theme default
- Text color (black/white) auto-computed from the chosen background's luminance for readability
- Applied via CSS custom properties `--mention-highlight-bg`, `--mention-highlight-text`, `--mention-highlight-border` set on `document.body`
- `CustomHtml.css.ts` uses these as CSS `var()` fallbacks over the original folds `Success` token colors
---
## Voice / Video Call Improvements
### Element Call Upgrade
@@ -589,6 +613,16 @@ A toggle in **Settings → Privacy** switches between sending `m.read` (public r
A leading emoji in a room name is rendered at 1.15× size in the sidebar for visual hierarchy. An emoji picker button (😊) is added to all room name input fields, prepending the selected emoji to the room name.
### Configurable Composer Toolbar (P3-6)
Users can individually show or hide each composer toolbar button in **Settings → Editor → Composer Toolbar Buttons**:
- Format Toggle, Emoji, Sticker, GIF, Location, Poll, Voice Message, Schedule Message
- All default to **on** — no visible change for existing users
- New buttons added in future will also default to on (deep-merge in `getSettings`)
- Send and Attach File buttons are not hideable
- Sticker still respects the existing `width < 500px` auto-hide on top of the setting
---
## Room Customization
@@ -691,6 +725,14 @@ A complete UI for managing Matrix push notification rules:
- Delete button for removable rules
- Add-rule form for creating new `room` and `sender` rules
### Notification Profile Presets (P5-27)
Three one-tap presets at the top of **Settings → Notifications** that apply a group of notification settings atomically:
- **Gaming 🎮** — notifications on, all sounds off (`messageSoundId: none`, `inviteSoundId: none`)
- **Work 💼** — all notifications and sounds on (restores defaults)
- **Sleep 🌙** — all notifications off (`showNotifications: false`, sounds off)
---
## Server Integration
+4 -4
View File
@@ -132,7 +132,7 @@ Status: `[ ]` pending · `[~]` in progress · `[x]` completed
---
### [ ] P3-6 · Configurable Composer Toolbar
### [x] P3-6 · Configurable Composer Toolbar
**What:** Let users rearrange or hide individual composer toolbar buttons (GIF, Sticker, Emoji, File, Voice, Location). Changes stored in `settingsAtom`. Access via a small "⚙ Customize toolbar" option in toolbar overflow.
**[AUDIT REQUIRED]** — Audit the current toolbar button rendering in `RoomInput.tsx`. Understand the layout system (is it a fixed array or already mapped from config?). Drag-to-reorder may require a DnD library; consider whether reorder is worth the complexity vs just toggle-visibility.
@@ -326,14 +326,14 @@ Themes:
---
### [ ] P5-21 · Custom @Mention Highlight Color
### [x] P5-21 · Custom @Mention Highlight Color
**What:** Each user sets their own mention highlight color in Settings → Appearance. Applied as `--user-mention-color` CSS property override on mention-highlighted message rows.
**Complexity:** Low.
---
### [ ] P5-22 · Font Selector for the UI
### [x] P5-22 · Font Selector for the UI
**What:** Font picker in Settings → Appearance. Options: JetBrains Mono, Inter, Geist, Fira Code, OpenDyslexic, System Default. Applied via CSS custom property overrides.
**[AUDIT REQUIRED]** Check if any fonts are already globally loaded to avoid double-loading.
@@ -341,7 +341,7 @@ Themes:
---
### [ ] P5-27 · Notification Profile Presets (Gaming / Work / Sleep)
### [x] P5-27 · Notification Profile Presets (Gaming / Work / Sleep)
**What:** Saved presets that change all notification settings atomically. Gaming (mentions only), Work (DMs + mentions), Sleep (all off). Quick-switch from sidebar or settings.
**Complexity:** Medium.
+1 -1
View File
@@ -30,7 +30,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap"
href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=VT323&display=swap"
rel="stylesheet"
/>
<link id="favicon" rel="shortcut icon" href="./public/favicon.ico" />
@@ -333,6 +333,11 @@ function Appearance() {
'glassmorphismSidebar',
);
const [pauseAnimations, setPauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
const [mentionHighlightColor, setMentionHighlightColor] = useSetting(
settingsAtom,
'mentionHighlightColor',
);
const [fontFamily, setFontFamily] = useSetting(settingsAtom, 'fontFamily');
return (
<Box direction="Column" gap="100">
@@ -494,6 +499,69 @@ function Appearance() {
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="UI Font"
description="Font used throughout the interface."
after={
<select
value={fontFamily ?? 'inter'}
onChange={(e) =>
setFontFamily(
e.target.value as 'system' | 'inter' | 'jetbrains-mono' | 'fira-code',
)
}
style={{
background: 'var(--bg-surface)',
color: 'inherit',
border: '1px solid var(--border-interactive-normal)',
borderRadius: '6px',
padding: '4px 8px',
fontSize: '14px',
cursor: 'pointer',
}}
>
<option value="system">System Default</option>
<option value="inter">Inter (default)</option>
<option value="jetbrains-mono">JetBrains Mono</option>
<option value="fira-code">Fira Code</option>
</select>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="@Mention Highlight Color"
description="Color used to highlight messages that mention you. Leave empty to use the theme default."
after={
<Box alignItems="Center" gap="200">
<input
type="color"
value={mentionHighlightColor || '#4caf50'}
onChange={(e) => setMentionHighlightColor(e.target.value)}
style={{ width: '36px', height: '28px', cursor: 'pointer', borderRadius: '4px', border: 'none', padding: '2px' }}
/>
{mentionHighlightColor && (
<button
type="button"
onClick={() => setMentionHighlightColor('')}
style={{
background: 'none',
border: '1px solid var(--border-interactive-normal)',
borderRadius: '6px',
padding: '2px 8px',
cursor: 'pointer',
color: 'inherit',
fontSize: '12px',
}}
>
Reset
</button>
)}
</Box>
}
/>
</SequenceCard>
</Box>
);
}
@@ -1,5 +1,6 @@
import React from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
import { useAtomValue, useSetAtom } from 'jotai';
import { Box, Text, IconButton, Icon, Icons, Scroll, config, toRem } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SystemNotification } from './SystemNotification';
import { AllMessagesNotifications } from './AllMessages';
@@ -9,6 +10,95 @@ import { PushRuleEditor } from './PushRuleEditor';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { settingsAtom, Settings } from '../../../state/settings';
const PRESETS: Array<{
label: string;
emoji: string;
description: string;
patch: Partial<Settings>;
}> = [
{
label: 'Gaming',
emoji: '🎮',
description: 'Notifications on, sounds off',
patch: {
showNotifications: true,
isNotificationSounds: false,
messageSoundId: 'none',
inviteSoundId: 'none',
quietHoursEnabled: false,
},
},
{
label: 'Work',
emoji: '💼',
description: 'All notifications and sounds on',
patch: {
showNotifications: true,
isNotificationSounds: true,
messageSoundId: 'notification',
inviteSoundId: 'invite',
quietHoursEnabled: false,
},
},
{
label: 'Sleep',
emoji: '🌙',
description: 'All notifications off',
patch: {
showNotifications: false,
isNotificationSounds: false,
quietHoursEnabled: false,
},
},
];
function NotificationPresets() {
const settings = useAtomValue(settingsAtom);
const setSettings = useSetAtom(settingsAtom);
const applyPreset = (patch: Partial<Settings>) => {
setSettings({ ...settings, ...patch });
};
return (
<Box direction="Column" gap="100">
<Text size="L400">Quick Presets</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<Box gap="300" style={{ padding: config.space.S300, flexWrap: 'wrap' }}>
{PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => applyPreset(preset.patch)}
title={preset.description}
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: toRem(4),
padding: `${toRem(8)} ${toRem(16)}`,
borderRadius: config.radii.R300,
border: '1px solid var(--border-interactive-normal)',
background: 'var(--bg-surface-low)',
color: 'inherit',
cursor: 'pointer',
minWidth: toRem(80),
}}
>
<span style={{ fontSize: toRem(24) }}>{preset.emoji}</span>
<Text size="T300" style={{ fontWeight: 600 }}>{preset.label}</Text>
<Text size="T200" priority="300" style={{ textAlign: 'center', maxWidth: toRem(120) }}>
{preset.description}
</Text>
</button>
))}
</Box>
</SequenceCard>
</Box>
);
}
type NotificationsProps = {
requestClose: () => void;
@@ -34,6 +124,7 @@ export function Notifications({ requestClose }: NotificationsProps) {
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
<NotificationPresets />
<SystemNotification />
<AllMessagesNotifications />
<SpecialMessagesNotifications />
+38 -1
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import * as Sentry from '@sentry/react';
import { Provider as JotaiProvider, useAtomValue } from 'jotai';
import { OverlayContainerProvider, PopOutContainerProvider, TooltipContainerProvider } from 'folds';
@@ -16,6 +16,42 @@ import { useCompositionEndTracking } from '../hooks/useComposingCheck';
import { settingsAtom } from '../state/settings';
import { LotusToastContainer } from '../features/toast/LotusToastContainer';
const FONT_MAP: Record<string, string> = {
system: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
inter: "'InterVariable', sans-serif",
'jetbrains-mono': "'JetBrains Mono', monospace",
'fira-code': "'Fira Code', monospace",
};
function AppearanceEffects() {
const settings = useAtomValue(settingsAtom);
useEffect(() => {
const color = settings.mentionHighlightColor;
if (color) {
document.body.style.setProperty('--mention-highlight-bg', color);
// compute black or white text based on hex luminance
const r = parseInt(color.slice(1, 3), 16);
const g = parseInt(color.slice(3, 5), 16);
const b = parseInt(color.slice(5, 7), 16);
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
document.body.style.setProperty('--mention-highlight-text', lum > 0.5 ? '#000' : '#fff');
document.body.style.setProperty('--mention-highlight-border', color);
} else {
document.body.style.removeProperty('--mention-highlight-bg');
document.body.style.removeProperty('--mention-highlight-text');
document.body.style.removeProperty('--mention-highlight-border');
}
}, [settings.mentionHighlightColor]);
useEffect(() => {
const font = FONT_MAP[settings.fontFamily ?? 'inter'] ?? FONT_MAP.inter;
document.body.style.setProperty('--font-secondary', font);
}, [settings.fontFamily]);
return null;
}
function NightLightOverlay() {
const settings = useAtomValue(settingsAtom);
if (!settings.nightLightEnabled) return null;
@@ -94,6 +130,7 @@ function App() {
<ClientConfigProvider value={clientConfig}>
<QueryClientProvider client={queryClient}>
<JotaiProvider>
<AppearanceEffects />
<RouterProvider router={createRouter(clientConfig, screenSize)} />
<NightLightOverlay />
<LotusToastContainer />
+6
View File
@@ -125,6 +125,9 @@ export interface Settings {
pauseAnimations: boolean;
composerToolbarButtons: ComposerToolbarSettings;
mentionHighlightColor: string;
fontFamily: 'system' | 'inter' | 'jetbrains-mono' | 'fira-code';
}
const defaultSettings: Settings = {
@@ -192,6 +195,9 @@ const defaultSettings: Settings = {
pauseAnimations: false,
composerToolbarButtons: DEFAULT_COMPOSER_TOOLBAR,
mentionHighlightColor: '',
fontFamily: 'inter',
};
export const getSettings = (): Settings => {
+3 -3
View File
@@ -169,9 +169,9 @@ export const Mention = recipe({
variants: {
highlight: {
true: {
backgroundColor: color.Success.Container,
color: color.Success.OnContainer,
boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.Success.ContainerLine}`,
backgroundColor: `var(--mention-highlight-bg, ${color.Success.Container as string})`,
color: `var(--mention-highlight-text, ${color.Success.OnContainer as string})`,
boxShadow: `0 0 0 ${config.borderWidth.B300} var(--mention-highlight-border, ${color.Success.ContainerLine as string})`,
},
},
focus: {