diff --git a/LOTUS_BUGS.md b/LOTUS_BUGS.md
index 319bd23d3..f276c8023 100644
--- a/LOTUS_BUGS.md
+++ b/LOTUS_BUGS.md
@@ -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`.
diff --git a/LOTUS_FEATURES.md b/LOTUS_FEATURES.md
index aa62f14ef..3a3f564a3 100644
--- a/LOTUS_FEATURES.md
+++ b/LOTUS_FEATURES.md
@@ -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 ``) 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
diff --git a/LOTUS_TODO.md b/LOTUS_TODO.md
index ee455a25c..d0ad3c7b8 100644
--- a/LOTUS_TODO.md
+++ b/LOTUS_TODO.md
@@ -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.
diff --git a/index.html b/index.html
index be26f5c96..2795cd8a8 100644
--- a/index.html
+++ b/index.html
@@ -30,7 +30,7 @@
diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx
index a30a6b2ce..efa9874c9 100644
--- a/src/app/features/settings/general/General.tsx
+++ b/src/app/features/settings/general/General.tsx
@@ -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 (
@@ -494,6 +499,69 @@ function Appearance() {
}
/>
+
+
+ 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',
+ }}
+ >
+
+
+
+
+
+ }
+ />
+
+
+
+ setMentionHighlightColor(e.target.value)}
+ style={{ width: '36px', height: '28px', cursor: 'pointer', borderRadius: '4px', border: 'none', padding: '2px' }}
+ />
+ {mentionHighlightColor && (
+
+ )}
+
+ }
+ />
+
);
}
diff --git a/src/app/features/settings/notifications/Notifications.tsx b/src/app/features/settings/notifications/Notifications.tsx
index 48545c663..8ac797015 100644
--- a/src/app/features/settings/notifications/Notifications.tsx
+++ b/src/app/features/settings/notifications/Notifications.tsx
@@ -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;
+}> = [
+ {
+ 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) => {
+ setSettings({ ...settings, ...patch });
+ };
+
+ return (
+
+ Quick Presets
+
+
+ {PRESETS.map((preset) => (
+
+ ))}
+
+
+
+ );
+}
type NotificationsProps = {
requestClose: () => void;
@@ -34,6 +124,7 @@ export function Notifications({ requestClose }: NotificationsProps) {
+
diff --git a/src/app/pages/App.tsx b/src/app/pages/App.tsx
index fdb2a4f01..322e15f32 100644
--- a/src/app/pages/App.tsx
+++ b/src/app/pages/App.tsx
@@ -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 = {
+ 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() {
+
diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts
index 2b37ef992..9d7881908 100644
--- a/src/app/state/settings.ts
+++ b/src/app/state/settings.ts
@@ -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 => {
diff --git a/src/app/styles/CustomHtml.css.ts b/src/app/styles/CustomHtml.css.ts
index 51f841b76..304d5b85e 100644
--- a/src/app/styles/CustomHtml.css.ts
+++ b/src/app/styles/CustomHtml.css.ts
@@ -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: {