Files
cinny/src/app/state/settings.ts
T
jared 702e2e00eb
CI / Build & Quality Checks (push) Successful in 10m54s
Trigger Desktop Build / trigger (push) Successful in 6s
feat: voice channel user limit (P5-10) + call join/leave sounds (P5-16)
P5-10 Voice Channel User Limit:
- New StateEvent.LotusVoiceLimit (io.lotus.voice_limit) with { max_users }
- RoomVoiceLimit admin control in Room Settings > General > Voice
  (power-level gated via permissions.stateEvent)
- CallPrescreen reads the limit reactively and disables Join with a
  'Channel Full (N/N)' message at capacity; existing members can rejoin

P5-16 Custom Join/Leave Sound Effects:
- useCallJoinLeaveSounds hook wired into CallUtils; detects participant
  join/leave via MatrixRTCSession membership changes (sender|deviceId),
  filters out self, only fires while joined
- Sounds synthesized in-browser with Web Audio (callSounds.ts) - no
  assets bundled; styles Off/Chime/Soft/Retro
- 'Join & Leave Sounds' selector in Settings > Calls (previews on change)

Docs: LOTUS_FEATURES.md, README.md, LOTUS_TODO.md (P5-10/P5-16 marked done)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:20:22 -04:00

248 lines
5.2 KiB
TypeScript

import { atom } from 'jotai';
const STORAGE_KEY = 'settings';
export type DateFormat =
| 'D MMM YYYY'
| 'DD/MM/YYYY'
| 'MM/DD/YYYY'
| 'YYYY/MM/DD'
| 'YYYY-MM-DD'
| '';
export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500';
export type ChatBackground =
| 'none'
| 'blueprint'
| 'carbon'
| 'stars'
| 'topographic'
| 'herringbone'
| 'crosshatch'
| 'chevron'
| 'polka'
| 'triangles'
| 'plaid'
| 'tactical'
| 'circuit'
| 'hexgrid'
| 'waves'
| 'neon'
| 'aurora'
| 'anim-rain'
| 'anim-stars'
| 'anim-pulse'
| 'anim-aurora'
| 'anim-fireflies';
export enum MessageLayout {
Modern = 0,
Compact = 1,
Bubble = 2,
}
export interface ComposerToolbarSettings {
showFormat: boolean;
showEmoji: boolean;
showSticker: boolean;
showGif: boolean;
showLocation: boolean;
showPoll: boolean;
showVoice: boolean;
showSchedule: boolean;
}
export const DEFAULT_COMPOSER_TOOLBAR: ComposerToolbarSettings = {
showFormat: true,
showEmoji: true,
showSticker: true,
showGif: true,
showLocation: true,
showPoll: true,
showVoice: true,
showSchedule: true,
};
export interface Settings {
themeId?: string;
useSystemTheme: boolean;
lightThemeId?: string;
darkThemeId?: string;
monochromeMode?: boolean;
isMarkdown: boolean;
editorToolbar: boolean;
twitterEmoji: boolean;
pageZoom: number;
hideActivity: boolean;
hidePresence: boolean;
privateReadReceipts: boolean;
presenceStatus: 'auto' | 'online' | 'idle' | 'dnd' | 'invisible';
isPeopleDrawer: boolean;
memberSortFilterIndex: number;
enterForNewline: boolean;
messageLayout: MessageLayout;
messageSpacing: MessageSpacing;
hideMembershipEvents: boolean;
hideNickAvatarEvents: boolean;
mediaAutoLoad: boolean;
urlPreview: boolean;
encUrlPreview: boolean;
showHiddenEvents: boolean;
legacyUsernameColor: boolean;
showNotifications: boolean;
isNotificationSounds: boolean;
messageSoundId: 'notification' | 'invite' | 'call' | 'none';
inviteSoundId: 'notification' | 'invite' | 'call' | 'none';
quietHoursEnabled: boolean;
quietHoursStart: string; // "HH:MM" 24h
quietHoursEnd: string; // "HH:MM" 24h
homeRoomSort: 'recent' | 'alpha' | 'unread';
hour24Clock: boolean;
dateFormatString: string;
developerTools: boolean;
lotusTerminal: boolean;
chatBackground: ChatBackground;
perMessageProfiles: boolean;
cameraOnJoin: boolean;
callNoiseSuppression: boolean;
pttMode: boolean;
pttKey: string;
nightLightEnabled: boolean;
nightLightOpacity: number;
glassmorphismSidebar: boolean;
deafenKey: string;
warnOnUnverifiedDevices: boolean;
pauseAnimations: boolean;
composerToolbarButtons: ComposerToolbarSettings;
mentionHighlightColor: string;
fontFamily: 'system' | 'inter' | 'jetbrains-mono' | 'fira-code';
afkAutoMute: boolean;
afkTimeoutMinutes: number;
callJoinLeaveSound: 'off' | 'chime' | 'soft' | 'retro';
}
const defaultSettings: Settings = {
themeId: undefined,
useSystemTheme: true,
lightThemeId: undefined,
darkThemeId: undefined,
monochromeMode: false,
isMarkdown: true,
editorToolbar: false,
twitterEmoji: false,
pageZoom: 100,
hideActivity: false,
hidePresence: false,
privateReadReceipts: false,
presenceStatus: 'auto',
isPeopleDrawer: true,
memberSortFilterIndex: 0,
enterForNewline: false,
messageLayout: 0,
messageSpacing: '400',
hideMembershipEvents: false,
hideNickAvatarEvents: true,
mediaAutoLoad: true,
urlPreview: true,
encUrlPreview: true,
showHiddenEvents: false,
legacyUsernameColor: false,
showNotifications: true,
isNotificationSounds: true,
messageSoundId: 'notification',
inviteSoundId: 'invite',
quietHoursEnabled: false,
quietHoursStart: '23:00',
quietHoursEnd: '08:00',
homeRoomSort: 'recent',
hour24Clock: false,
dateFormatString: 'D MMM YYYY',
developerTools: false,
lotusTerminal: false,
chatBackground: 'none',
perMessageProfiles: false,
cameraOnJoin: false,
callNoiseSuppression: true,
pttMode: false,
pttKey: 'Space',
nightLightEnabled: false,
nightLightOpacity: 30,
glassmorphismSidebar: false,
deafenKey: 'KeyM',
warnOnUnverifiedDevices: false,
pauseAnimations: false,
composerToolbarButtons: DEFAULT_COMPOSER_TOOLBAR,
mentionHighlightColor: '',
fontFamily: 'inter',
afkAutoMute: false,
afkTimeoutMinutes: 10,
callJoinLeaveSound: 'chime',
};
export const getSettings = (): Settings => {
try {
const settings = localStorage.getItem(STORAGE_KEY);
if (settings === null) return defaultSettings;
const saved = JSON.parse(settings) as Partial<Settings>;
return {
...defaultSettings,
...saved,
composerToolbarButtons: {
...DEFAULT_COMPOSER_TOOLBAR,
...(saved.composerToolbarButtons ?? {}),
},
};
} catch {
localStorage.removeItem(STORAGE_KEY);
return defaultSettings;
}
};
export const setSettings = (settings: Settings) => {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
} catch {
/* quota */
}
};
const baseSettings = atom<Settings>(getSettings());
export const settingsAtom = atom<Settings, [Settings], undefined>(
(get) => get(baseSettings),
(get, set, update) => {
set(baseSettings, update);
setSettings(update);
},
);