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:
2026-07-01 10:01:10 -04:00
parent 7e38baa7b6
commit 4509a2b6d3
8 changed files with 294 additions and 7 deletions
+9 -2
View File
@@ -2,6 +2,7 @@ import { useAtomValue, useSetAtom } from 'jotai';
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { RoomEvent, RoomEventHandlerMap } from 'matrix-js-sdk';
import { focusAssistActiveAtom } from '../../state/focusAssist';
import { roomToUnreadAtom, unreadEqual, unreadInfoToUnread } from '../../state/room/roomToUnread';
import LogoSVG from '../../../../public/res/lotus.png';
import LogoUnreadSVG from '../../../../public/res/lotus-unread.png';
@@ -110,6 +111,7 @@ function InviteNotifications() {
const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
const [quietHoursEnabled] = useSetting(settingsAtom, 'quietHoursEnabled');
const focusAssistActive = useAtomValue(focusAssistActiveAtom);
const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart');
const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd');
const [inviteSoundId] = useSetting(settingsAtom, 'inviteSoundId');
@@ -168,7 +170,8 @@ function InviteNotifications() {
useEffect(() => {
if (invites.length > perviousInviteLen && mx.getSyncState() === 'SYNCING') {
const quietActive = quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd);
const quietActive =
focusAssistActive || (quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd));
if (!quietActive) {
if (showNotifications && notificationPermission('granted')) {
notify(invites.length - perviousInviteLen);
@@ -190,6 +193,7 @@ function InviteNotifications() {
quietHoursEnabled,
quietHoursStart,
quietHoursEnd,
focusAssistActive,
inviteSoundId,
]);
@@ -213,6 +217,7 @@ function MessageNotifications() {
const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
const [quietHoursEnabled] = useSetting(settingsAtom, 'quietHoursEnabled');
const focusAssistActive = useAtomValue(focusAssistActiveAtom);
const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart');
const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd');
const [messageSoundId] = useSetting(settingsAtom, 'messageSoundId');
@@ -356,7 +361,8 @@ function MessageNotifications() {
return;
}
const quietActive = quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd);
const quietActive =
focusAssistActive || (quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd));
if (!quietActive) {
if (showNotifications && notificationPermission('granted')) {
const avatarMxc =
@@ -395,6 +401,7 @@ function MessageNotifications() {
quietHoursEnabled,
quietHoursStart,
quietHoursEnd,
focusAssistActive,
messageSoundId,
]);