feat: P5-21 mention color, P5-22 font selector, P5-27 notification presets; update docs
- 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:
+3
-26
@@ -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`.
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 />
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user