702e2e00eb
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>
248 lines
5.2 KiB
TypeScript
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);
|
|
},
|
|
);
|