feat(desktop): Tier A desktop features — web side (P5-46/36/44/43/49/47/55/57)
Web half of the desktop feature wave. A shared bridge (`hooks/useTauri.ts`: invokeTauri/isTauri/useTauriEvent) backs per-feature hooks that no-op in the browser and drive the native Tauri commands (compiled in cinny-desktop): - P5-46 useTauriCallPower — hold system awake while a call is active. - P5-36 useTauriJumpList — Windows jump list of recent rooms → matrix: deep links. - P5-44 useTauriThumbbar — taskbar Mute/Deafen/End; events toggle mic/sound/hangup. - P5-43 useTauriSmtc — SMTC call state + button events. - P5-49 useTauriNetwork — react to native network-change → mx.retryImmediately(). - P5-47 window chrome — opt-in `customWindowChromeAtom` + TDS `TitleBar`; DesktopChrome wrapper in App.tsx (zero layout impact when off) + a desktop-only settings toggle. - P5-55 composer toolbar drag-reorder (settings order[] + pragmatic-drag-and-drop). - P5-57 DraftIndicator — subtle "draft saved" cue in the composer. Client-scoped hooks mount via TauriDesktopFeatures in ClientNonUIFeatures; window chrome mounts at App level. Gates: tsc/eslint/prettier clean, build OK, 556 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
atomWithLocalStorage,
|
||||
getLocalStorageItem,
|
||||
setLocalStorageItem,
|
||||
} from './utils/atomWithLocalStorage';
|
||||
|
||||
const CUSTOM_WINDOW_CHROME = 'customWindowChrome';
|
||||
|
||||
/**
|
||||
* P5-47 — TDS Custom Window Chrome opt-in flag (default `false`).
|
||||
*
|
||||
* Standalone, `localStorage`-backed boolean atom kept separate from
|
||||
* `state/settings.ts` on purpose. When `true` (and running inside Tauri) the app
|
||||
* strips the native window frame and renders its own `<TitleBar/>`; when `false`
|
||||
* the native OS frame is used. The feature is runtime-reversible, so flipping
|
||||
* this atom is all it takes to switch back and forth.
|
||||
*/
|
||||
export const customWindowChromeAtom = atomWithLocalStorage<boolean>(
|
||||
CUSTOM_WINDOW_CHROME,
|
||||
(key) => getLocalStorageItem<boolean>(key, false),
|
||||
(key, value) => setLocalStorageItem(key, value),
|
||||
);
|
||||
@@ -60,6 +60,39 @@ export enum MessageLayout {
|
||||
Bubble = 2,
|
||||
}
|
||||
|
||||
/**
|
||||
* Keys of the toggleable composer toolbar buttons. Also used as the identity
|
||||
* of each button when persisting/restoring a custom drag-and-drop order.
|
||||
*/
|
||||
export const COMPOSER_TOOLBAR_BUTTON_KEYS = [
|
||||
'showFormat',
|
||||
'showEmoji',
|
||||
'showSticker',
|
||||
'showGif',
|
||||
'showLocation',
|
||||
'showPoll',
|
||||
'showVoice',
|
||||
'showSchedule',
|
||||
] as const;
|
||||
|
||||
export type ComposerToolbarButtonKey = (typeof COMPOSER_TOOLBAR_BUTTON_KEYS)[number];
|
||||
|
||||
/**
|
||||
* The fixed order the composer toolbar rendered before reordering existed.
|
||||
* Used as the fallback for users without a saved order, and to append any
|
||||
* new/unknown button keys, so existing users see no change.
|
||||
*/
|
||||
export const DEFAULT_COMPOSER_TOOLBAR_ORDER: ComposerToolbarButtonKey[] = [
|
||||
'showFormat',
|
||||
'showSticker',
|
||||
'showEmoji',
|
||||
'showGif',
|
||||
'showLocation',
|
||||
'showPoll',
|
||||
'showVoice',
|
||||
'showSchedule',
|
||||
];
|
||||
|
||||
export interface ComposerToolbarSettings {
|
||||
showFormat: boolean;
|
||||
showEmoji: boolean;
|
||||
@@ -69,6 +102,7 @@ export interface ComposerToolbarSettings {
|
||||
showPoll: boolean;
|
||||
showVoice: boolean;
|
||||
showSchedule: boolean;
|
||||
order: ComposerToolbarButtonKey[];
|
||||
}
|
||||
|
||||
export const DEFAULT_COMPOSER_TOOLBAR: ComposerToolbarSettings = {
|
||||
@@ -80,6 +114,37 @@ export const DEFAULT_COMPOSER_TOOLBAR: ComposerToolbarSettings = {
|
||||
showPoll: true,
|
||||
showVoice: true,
|
||||
showSchedule: true,
|
||||
order: DEFAULT_COMPOSER_TOOLBAR_ORDER,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a complete, de-duplicated composer toolbar order:
|
||||
* - drops unknown/duplicate keys from the saved order
|
||||
* - appends any missing keys (new buttons or existing users with no saved
|
||||
* order) at the end in their canonical default position
|
||||
* so a button can never disappear from the toolbar.
|
||||
*/
|
||||
export const normalizeComposerToolbarOrder = (
|
||||
order: ComposerToolbarButtonKey[] | undefined,
|
||||
): ComposerToolbarButtonKey[] => {
|
||||
const known = new Set<ComposerToolbarButtonKey>(COMPOSER_TOOLBAR_BUTTON_KEYS);
|
||||
const seen = new Set<ComposerToolbarButtonKey>();
|
||||
const result: ComposerToolbarButtonKey[] = [];
|
||||
|
||||
(order ?? []).forEach((key) => {
|
||||
if (known.has(key) && !seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(key);
|
||||
}
|
||||
});
|
||||
DEFAULT_COMPOSER_TOOLBAR_ORDER.forEach((key) => {
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
result.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export interface Settings {
|
||||
@@ -318,6 +383,7 @@ export const getSettings = (): Settings => {
|
||||
composerToolbarButtons: {
|
||||
...DEFAULT_COMPOSER_TOOLBAR,
|
||||
...(saved.composerToolbarButtons ?? {}),
|
||||
order: normalizeComposerToolbarOrder(saved.composerToolbarButtons?.order),
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user