feat(desktop): Tier B web side — toast actions, Focus Assist gate, folder DnD
- P5-41/35 useTauriToastActions: native rich-toast click → navigate(path) (opens the room), quick reply → mx.sendMessage(roomId, m.text). The desktop bridge routes message notifications (tag=roomId) to show_rich_toast. - P5-56 useTauriFocusAssist + focusAssistActiveAtom: a native focus-assist-changed event drives the atom, OR'd into the existing quiet-hours gate in ClientNonUIFeatures so notifications+sounds suppress during Windows Focus Assist. - P5-48 recursive folder drag-drop: fileEntries.ts (sync webkitGetAsEntry capture → async batched readEntries traversal, path-prefixed names, 500-file cap) wired into useFileDrop, reusing the existing upload pipeline. +3 unit tests. Hooks no-op in the browser. Gates: tsc/eslint/prettier clean, build OK, 559 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { useCallback, DragEventHandler, RefObject, useState, useEffect, useRef } from 'react';
|
||||
import { getDataTransferFiles } from '../utils/dom';
|
||||
import { collectDroppedFiles } from '../utils/fileEntries';
|
||||
|
||||
export const useFileDropHandler = (onDrop: (file: File[]) => void): DragEventHandler =>
|
||||
useCallback(
|
||||
(evt) => {
|
||||
const files = getDataTransferFiles(evt.dataTransfer);
|
||||
if (files) onDrop(files);
|
||||
// `collectDroppedFiles` synchronously captures the entry list from the
|
||||
// DataTransfer before traversing folders asynchronously.
|
||||
collectDroppedFiles(evt.dataTransfer).then((files) => {
|
||||
if (files) onDrop(files);
|
||||
});
|
||||
},
|
||||
[onDrop],
|
||||
);
|
||||
@@ -24,8 +27,12 @@ export const useFileDropZone = (
|
||||
dragCounterRef.current = 0;
|
||||
setActive(false);
|
||||
if (!evt.dataTransfer) return;
|
||||
const files = getDataTransferFiles(evt.dataTransfer);
|
||||
if (files) onDrop(files);
|
||||
// Capture entries synchronously (inside the event) then traverse any
|
||||
// dropped folders asynchronously — the DataTransferItemList is emptied
|
||||
// once this handler returns.
|
||||
collectDroppedFiles(evt.dataTransfer).then((files) => {
|
||||
if (files) onDrop(files);
|
||||
});
|
||||
};
|
||||
|
||||
target?.addEventListener('drop', handleDrop);
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { focusAssistActiveAtom } from '../state/focusAssist';
|
||||
import { useTauriEvent } from './useTauri';
|
||||
|
||||
/** Detail shape of the `focus-assist-changed` event emitted by the native side. */
|
||||
type FocusAssistChangedDetail = {
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* P5-56 — Windows Focus Assist ↔ Do-Not-Disturb sync (desktop). Subscribes to
|
||||
* the native `focus-assist-changed` event (Windows `SHQueryUserNotificationState`
|
||||
* poll, `{ active }`) and mirrors it into `focusAssistActiveAtom`, which the
|
||||
* notification gate reads to suppress notifications while the shell is in Focus
|
||||
* Assist / Quiet Hours, presenting, gaming full-screen, or busy. Inert in the
|
||||
* browser, since `useTauriEvent` only listens under Tauri.
|
||||
*/
|
||||
export function useTauriFocusAssist(): void {
|
||||
const setFocusAssist = useSetAtom(focusAssistActiveAtom);
|
||||
|
||||
useTauriEvent<FocusAssistChangedDetail>('focus-assist-changed', ({ active }) =>
|
||||
setFocusAssist(active),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { MsgType } from 'matrix-js-sdk';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useTauriEvent } from './useTauri';
|
||||
|
||||
/** Payload of the `lotus-notification-activate` event (a plain body click). */
|
||||
interface ActivateDetail {
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/** Payload of the `lotus-notification-reply` event (the inline reply box). */
|
||||
interface ReplyDetail {
|
||||
roomId?: string;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-41 / P5-35 — wire the native WinRT toast's click + quick-reply back into the
|
||||
* client. The Rust side (`show_rich_toast`) dispatches DOM CustomEvents via
|
||||
* `emit_to_web`:
|
||||
* - `lotus-notification-activate` → route to the room the toast was for, reusing
|
||||
* the same `useNavigate(path)` mechanism the web `notificationclick` path uses
|
||||
* (see ClientNonUIFeatures).
|
||||
* - `lotus-notification-reply` → send the typed reply straight to the room.
|
||||
* No-op outside Tauri (the events never fire).
|
||||
*/
|
||||
export function useTauriToastActions(): void {
|
||||
const navigate = useNavigate();
|
||||
const mx = useMatrixClient();
|
||||
|
||||
useTauriEvent<ActivateDetail>('lotus-notification-activate', ({ path }) => {
|
||||
if (path) navigate(path);
|
||||
});
|
||||
|
||||
useTauriEvent<ReplyDetail>('lotus-notification-reply', ({ roomId, text }) => {
|
||||
if (!roomId || !text) return;
|
||||
mx.sendMessage(roomId, { msgtype: MsgType.Text, body: text }).catch(() => undefined);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user