66 lines
2.0 KiB
TypeScript
66 lines
2.0 KiB
TypeScript
/*
|
|||
|
|
Copyright 2026 Lotus Guild
|
||
|
|
|
||
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||
|
|
Please see LICENSE in the repository root for full details.
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* [element-call #36] Remember the per-participant volume slider across calls,
|
||
|
|
* reloads and reconnects. Keyed by Matrix user id (not device), so "Bob is
|
||
|
|
* loud" sticks when Bob switches devices. Local to this browser, never synced.
|
||
|
|
* Only voice is remembered; screenshare audio stays per-share.
|
||
|
|
*/
|
||
|
|
|
||
|
|
const STORAGE_KEY = "lotus-per-user-volume";
|
||
|
|
/** Most recently set entries kept; older ones are dropped. */
|
||
|
|
export const MAX_REMEMBERED = 50;
|
||
|
|
|
||
|
|
type VolumeMap = Record<string, number>;
|
||
|
|
|
||
|
|
const isVolume = (v: unknown): v is number =>
|
||
|
|
typeof v === "number" && Number.isFinite(v) && v >= 0 && v <= 4;
|
||
|
|
|
||
|
|
function load(): VolumeMap {
|
||
|
|
try {
|
||
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||
|
|
if (!raw) return {};
|
||
|
|
const parsed: unknown = JSON.parse(raw);
|
||
|
|
if (typeof parsed !== "object" || parsed === null) return {};
|
||
|
|
const out: VolumeMap = {};
|
||
|
|
for (const [k, v] of Object.entries(parsed)) if (isVolume(v)) out[k] = v;
|
||
|
|
return out;
|
||
|
|
} catch {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function save(map: VolumeMap): void {
|
||
|
|
try {
|
||
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
|
||
|
|
} catch {
|
||
|
|
// Storage unavailable (private mode, quota): the slider just won't stick.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/** The remembered volume for `userId`, or 1 (100 %) if none. */
|
||
|
|
export function getRememberedVolume(userId: string): number {
|
||
|
|
return load()[userId] ?? 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Remember `volume` for `userId`. 1 (the default) forgets the entry. The entry
|
||
|
|
* moves to the end so the map is in least-recently-set order, trimmed to
|
||
|
|
* MAX_REMEMBERED.
|
||
|
|
*/
|
||
|
|
export function rememberVolume(userId: string, volume: number): void {
|
||
|
|
if (!isVolume(volume)) return;
|
||
|
|
const map = load();
|
||
|
|
delete map[userId];
|
||
|
|
if (volume !== 1) map[userId] = volume;
|
||
|
|
const keys = Object.keys(map);
|
||
|
|
for (const k of keys.slice(0, Math.max(0, keys.length - MAX_REMEMBERED)))
|
||
|
|
delete map[k];
|
||
|
|
save(map);
|
||
|
|
}
|