fix(desktop): address code-review findings on the desktop wave
CI / Build & Quality Checks (push) Successful in 10m40s
CI / Trigger Desktop Build (push) Successful in 8s

- fileEntries: a single unreadable file/dir in a dropped folder no longer aborts
  the whole traversal (try/catch per entry, skip failures) — was discarding ALL
  dropped files (incl. the flat-file path) + an unhandled rejection; also add
  .catch in both useFileDrop consumers.
- RoomInput: mirror a localStorage-restored draft into the draft atom so the
  P5-57 indicator reflects a persisted draft after a page reload, not only on
  same-session room re-entry.
- useTauriThumbbar: swallow toggleMicrophone()/hangup() rejections (parity with
  SMTC) — avoids an unhandled rejection when clicked mid-teardown.
- App/DesktopChrome: keep wrapper element types stable across the chrome toggle
  (display:contents when off) so flipping it no longer remounts RouterProvider.
- settings: normalizeComposerToolbarOrder also appends missing keys from the
  canonical key set (safety net if a new button is absent from the default order).

Gates: tsc/eslint/prettier clean, build OK, 559 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 10:40:31 -04:00
parent 3336abb66f
commit 258e3ec620
6 changed files with 62 additions and 18 deletions
+5 -1
View File
@@ -369,13 +369,17 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
const nodes = JSON.parse(stored); const nodes = JSON.parse(stored);
if (Array.isArray(nodes) && nodes.length > 0) { if (Array.isArray(nodes) && nodes.length > 0) {
Transforms.insertFragment(editor, nodes); Transforms.insertFragment(editor, nodes);
// Mirror the restored draft into the atom so the draft indicator
// (reads roomIdToMsgDraftAtomFamily) reflects a persisted draft
// after a page reload — not only on same-session room re-entry.
setMsgDraft(nodes);
} }
} }
} catch { } catch {
// Ignore malformed stored draft // Ignore malformed stored draft
} }
} }
}, [editor, msgDraft, roomId]); }, [editor, msgDraft, roomId, setMsgDraft]);
useEffect( useEffect(
() => () => { () => () => {
+10 -6
View File
@@ -6,9 +6,11 @@ export const useFileDropHandler = (onDrop: (file: File[]) => void): DragEventHan
(evt) => { (evt) => {
// `collectDroppedFiles` synchronously captures the entry list from the // `collectDroppedFiles` synchronously captures the entry list from the
// DataTransfer before traversing folders asynchronously. // DataTransfer before traversing folders asynchronously.
collectDroppedFiles(evt.dataTransfer).then((files) => { collectDroppedFiles(evt.dataTransfer)
if (files) onDrop(files); .then((files) => {
}); if (files) onDrop(files);
})
.catch(() => undefined);
}, },
[onDrop], [onDrop],
); );
@@ -30,9 +32,11 @@ export const useFileDropZone = (
// Capture entries synchronously (inside the event) then traverse any // Capture entries synchronously (inside the event) then traverse any
// dropped folders asynchronously — the DataTransferItemList is emptied // dropped folders asynchronously — the DataTransferItemList is emptied
// once this handler returns. // once this handler returns.
collectDroppedFiles(evt.dataTransfer).then((files) => { collectDroppedFiles(evt.dataTransfer)
if (files) onDrop(files); .then((files) => {
}); if (files) onDrop(files);
})
.catch(() => undefined);
}; };
target?.addEventListener('drop', handleDrop); target?.addEventListener('drop', handleDrop);
+3 -2
View File
@@ -31,13 +31,14 @@ export function useTauriThumbbar(): void {
if (!callEmbed) return; if (!callEmbed) return;
if (action === 'mute') { if (action === 'mute') {
// toggleMicrophone flips the mic; `microphone === false` means muted. // toggleMicrophone flips the mic; `microphone === false` means muted.
callEmbed.control.toggleMicrophone(); // Async transport send — swallow rejection (widget mid-teardown), as SMTC does.
callEmbed.control.toggleMicrophone().catch(() => undefined);
} else if (action === 'deafen') { } else if (action === 'deafen') {
// toggleSound flips local audio; `sound === false` means deafened. It also // toggleSound flips local audio; `sound === false` means deafened. It also
// mutes the mic while deafened, matching the in-app Deafen control. // mutes the mic while deafened, matching the in-app Deafen control.
callEmbed.control.toggleSound(); callEmbed.control.toggleSound();
} else if (action === 'end') { } else if (action === 'end') {
callEmbed.hangup(); callEmbed.hangup().catch(() => undefined);
} }
}); });
} }
+17 -4
View File
@@ -100,11 +100,24 @@ function TauriEffects() {
function DesktopChrome({ children }: { children: ReactNode }) { function DesktopChrome({ children }: { children: ReactNode }) {
const customChrome = useAtomValue(customWindowChromeAtom); const customChrome = useAtomValue(customWindowChromeAtom);
useTauriWindowChrome(); useTauriWindowChrome();
if (!(isTauri() && customChrome)) return <>{children}</>; const useChrome = isTauri() && customChrome;
// Keep the wrapper element structure STABLE across the toggle so flipping the
// setting never changes the element type in `children`'s ancestry — otherwise
// React would unmount/remount the whole RouterProvider subtree (losing scroll,
// menus, unsaved composer state). When off, both wrappers use `display:contents`
// so they generate no box → zero layout impact (also the browser default path).
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100vh' }}> <div
<TitleBar /> style={
<div style={{ flexGrow: 1, minHeight: 0 }}>{children}</div> useChrome
? { display: 'flex', flexDirection: 'column', height: '100vh' }
: { display: 'contents' }
}
>
{useChrome && <TitleBar />}
<div style={useChrome ? { flexGrow: 1, minHeight: 0 } : { display: 'contents' }}>
{children}
</div>
</div> </div>
); );
} }
+10
View File
@@ -137,12 +137,22 @@ export const normalizeComposerToolbarOrder = (
result.push(key); result.push(key);
} }
}); });
// Append missing keys in their canonical default position…
DEFAULT_COMPOSER_TOOLBAR_ORDER.forEach((key) => { DEFAULT_COMPOSER_TOOLBAR_ORDER.forEach((key) => {
if (!seen.has(key)) { if (!seen.has(key)) {
seen.add(key); seen.add(key);
result.push(key); result.push(key);
} }
}); });
// …then any known key not covered by the default order (safety net so a new
// button added to COMPOSER_TOOLBAR_BUTTON_KEYS but forgotten in the default
// order can still render/reorder rather than being permanently dropped).
COMPOSER_TOOLBAR_BUTTON_KEYS.forEach((key) => {
if (!seen.has(key)) {
seen.add(key);
result.push(key);
}
});
return result; return result;
}; };
+17 -5
View File
@@ -77,16 +77,28 @@ export const filesFromEntries = async (
const walk = async (entry: FileSystemEntry, prefix: string): Promise<void> => { const walk = async (entry: FileSystemEntry, prefix: string): Promise<void> => {
if (files.length >= maxFiles) return; if (files.length >= maxFiles) return;
// A single unreadable file/directory (moved between drop and read, a
// permissions/lock error, an OS special file) must NOT abort the whole
// traversal — skip it and keep collecting the rest.
if (entry.isFile) { if (entry.isFile) {
const file = await fileFromFileEntry(entry as FileSystemFileEntry); try {
if (files.length >= maxFiles) return; const file = await fileFromFileEntry(entry as FileSystemFileEntry);
files.push(prefix ? renameFile(file, `${prefix}${file.name}`) : file); if (files.length >= maxFiles) return;
files.push(prefix ? renameFile(file, `${prefix}${file.name}`) : file);
} catch {
/* skip unreadable file */
}
return; return;
} }
if (entry.isDirectory) { if (entry.isDirectory) {
const reader = (entry as FileSystemDirectoryEntry).createReader(); let childEntries: FileSystemEntry[] = [];
const childEntries = await readAllDirectoryEntries(reader); try {
const reader = (entry as FileSystemDirectoryEntry).createReader();
childEntries = await readAllDirectoryEntries(reader);
} catch {
return; /* skip unreadable directory */
}
const childPrefix = `${prefix}${entry.name}/`; const childPrefix = `${prefix}${entry.name}/`;
for (const child of childEntries) { for (const child of childEntries) {
if (files.length >= maxFiles) break; if (files.length >= maxFiles) break;