@rapidmx/web-client 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +373 -373
- package/apps/admin/branding/index.tsx +39 -39
- package/apps/admin/data-requests/index.tsx +482 -480
- package/apps/admin/domains/[uid].tsx +164 -164
- package/apps/admin/escrow-scopes/[uid].tsx +348 -348
- package/apps/admin/index.tsx +127 -124
- package/apps/admin/mailboxes/[uid].tsx +269 -224
- package/apps/admin/mailboxes/new/index.tsx +28 -28
- package/apps/admin/plugins/index.tsx +15 -15
- package/apps/admin/retention-policy/index.tsx +39 -39
- package/apps/admin/signing-certificates/index.tsx +343 -343
- package/apps/escrow/audit-log/index.tsx +196 -196
- package/apps/escrow/matters/[uid].tsx +621 -621
- package/apps/shared/auth/adminAccess.ts +99 -99
- package/apps/shared/components/admin/layout/AdminShell.tsx +378 -378
- package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +238 -0
- package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -152
- package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +194 -0
- package/apps/shared/components/admin/settings/BrandingForm.tsx +423 -423
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +119 -119
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +50 -3
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +198 -198
- package/apps/shared/components/admin/settings/PluginsManager.tsx +1545 -1545
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +182 -182
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +288 -288
- package/apps/shared/components/admin/setup/SetupWizard.tsx +444 -444
- package/apps/shared/components/admin/usePagedList.tsx +129 -129
- package/apps/shared/components/calendar/EventEditor.tsx +5 -10
- package/apps/shared/components/calendar/EventFormParts.tsx +16 -47
- package/apps/shared/components/calendar/EventModal.tsx +206 -206
- package/apps/shared/components/calendar/GuestInput.tsx +100 -0
- package/apps/shared/components/calendar/MonthView.tsx +185 -185
- package/apps/shared/components/calendar/RecurrenceEditor.tsx +227 -227
- package/apps/shared/components/calendar/RequestChangeForm.tsx +23 -54
- package/apps/shared/components/calendar/SplitDayView.tsx +144 -144
- package/apps/shared/components/calendar/TimeGridView.tsx +246 -246
- package/apps/shared/components/calendar/allDay.ts +124 -124
- package/apps/shared/components/calendar/eventForm.ts +2 -2
- package/apps/shared/components/calendar/eventFormat.ts +49 -22
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +201 -200
- package/apps/shared/components/contacts/ContactForm.tsx +383 -383
- package/apps/shared/components/contacts/ContactsSidebar.tsx +1 -1
- package/apps/shared/components/contacts/ContactsToolbar.tsx +103 -98
- package/apps/shared/components/contacts/layout/ContactsShell.tsx +217 -216
- package/apps/shared/components/escrow/layout/EscrowShell.tsx +155 -155
- package/apps/shared/components/layout/AppShell.tsx +480 -480
- package/apps/shared/components/layout/FloatingActionButton.tsx +31 -0
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +398 -398
- package/apps/shared/components/layout/MailboxProvisioning.tsx +168 -168
- package/apps/shared/components/layout/ResponsiveToolbar.tsx +315 -315
- package/apps/shared/components/layout/ThemeSwitch.tsx +84 -84
- package/apps/shared/components/layout/UserMenu.tsx +439 -424
- package/apps/shared/components/mail/ConversationList.tsx +323 -323
- package/apps/shared/components/mail/ConversationThreadPane.tsx +612 -491
- package/apps/shared/components/mail/MailSelectionBar.tsx +236 -227
- package/apps/shared/components/mail/MessageDetailPane.tsx +1559 -1559
- package/apps/shared/components/mail/compose/ComposeContext.tsx +312 -312
- package/apps/shared/components/mail/compose/ComposeToolbar.tsx +422 -422
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +1681 -1677
- package/apps/shared/components/mail/compose/RecipientInput.tsx +54 -11
- package/apps/shared/components/mail/compose/RichTextEditor.tsx +147 -147
- package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -60
- package/apps/shared/components/mail/compose/quotedBody.ts +161 -161
- package/apps/shared/components/mail/layout/FolderBadgeChip.tsx +21 -0
- package/apps/shared/components/mail/layout/MailShell.tsx +137 -113
- package/apps/shared/components/mail/layout/SidebarSection.tsx +66 -0
- package/apps/shared/components/mail/reading/PendingMessageCard.tsx +68 -0
- package/apps/shared/components/mail/verificationSeals.ts +125 -125
- package/apps/shared/components/rules/RuleBuilder.tsx +311 -311
- package/apps/shared/components/settings/SigningCertificateCard.tsx +344 -344
- package/apps/shared/components/settings/layout/SettingsShell.tsx +255 -254
- package/apps/shared/components/tasks/TasksSidebar.tsx +1 -1
- package/apps/shared/components/tasks/layout/TasksShell.tsx +218 -217
- package/apps/shared/keyboard/GlobalShortcuts.tsx +51 -51
- package/apps/shared/keyboard/ShortcutProvider.tsx +62 -62
- package/apps/shared/keyboard/ShortcutsDialog.tsx +84 -84
- package/apps/shared/keyboard/dispatch.ts +124 -124
- package/apps/shared/keyboard/format.ts +89 -89
- package/apps/shared/keyboard/keymap.ts +114 -114
- package/apps/shared/keyboard/registry.ts +65 -65
- package/apps/shared/keyboard/targets.ts +79 -79
- package/apps/shared/mail/folderCounts.ts +17 -0
- package/apps/shared/mail/folderTree.ts +143 -143
- package/apps/shared/mail/listAllPages.ts +39 -39
- package/apps/shared/mail/newMailNotifications.ts +171 -171
- package/apps/shared/mail/outbox/outgoingReplies.ts +192 -0
- package/apps/shared/mail/outbox/sendJob.ts +19 -1
- package/apps/shared/mail/outbox/sendOutcomes.ts +13 -4
- package/apps/shared/mail/primaryMailbox.ts +71 -0
- package/apps/shared/mail/useCollapsedSections.ts +146 -0
- package/apps/shared/mail/useMailConnection.ts +205 -203
- package/apps/shared/mail/useMailLiveUpdates.ts +277 -277
- package/apps/shared/mail/useMarkMessageRead.ts +47 -47
- package/apps/shared/mail/useNewMailNotifications.ts +178 -178
- package/apps/shared/navigation/AppRouter.tsx +300 -300
- package/apps/shared/navigation/routerContext.tsx +83 -83
- package/apps/shared/notifications/store.ts +560 -560
- package/apps/shared/search/LocalIndexLifecycle.tsx +114 -114
- package/apps/shared/search/crossMailboxSearch.ts +169 -0
- package/apps/shared/search/localIndexBuilder.ts +481 -481
- package/apps/shared/signing/enrollmentStorage.ts +33 -33
- package/apps/shared/signing/enrollmentTracker.ts +385 -385
- package/apps/shared/signing/enrollmentView.ts +251 -251
- package/apps/shared/signing/useNow.ts +19 -19
- package/apps/shared/signing/useSigningEnrollmentWatcher.ts +90 -90
- package/apps/shared/styles/app.css +396 -386
- package/apps/www/calendar/index.tsx +578 -571
- package/apps/www/contacts/[uid].tsx +109 -109
- package/apps/www/contacts/index.tsx +6 -0
- package/apps/www/index.tsx +2847 -2479
- package/apps/www/messages/[uid].tsx +135 -135
- package/apps/www/settings/auto-reply/index.tsx +133 -133
- package/apps/www/settings/encryption/index.tsx +1287 -1287
- package/apps/www/settings/filters/[uid].tsx +176 -176
- package/apps/www/settings/filters/index.tsx +102 -102
- package/apps/www/settings/filters/new/index.tsx +140 -140
- package/apps/www/settings/labels/index.tsx +204 -204
- package/apps/www/settings/privacy/index.tsx +492 -492
- package/apps/www/settings/profile/index.tsx +248 -248
- package/apps/www/settings/read-receipts/index.tsx +147 -147
- package/apps/www/settings/sharing/index.tsx +256 -256
- package/apps/www/settings/signatures/[uid].tsx +172 -172
- package/apps/www/settings/signatures/index.tsx +88 -88
- package/apps/www/settings/signatures/new/index.tsx +135 -135
- package/apps/www/tasks/index.tsx +649 -649
- package/dist/apps/admin/data-requests/index.js +1 -1
- package/dist/apps/admin/index.js +2 -1
- package/dist/apps/admin/mailboxes/[uid].js +12 -2
- package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -2
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.d.ts +35 -0
- package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +115 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.d.ts +16 -0
- package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +92 -0
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +3 -3
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +25 -3
- package/dist/apps/shared/components/calendar/EventEditor.js +5 -10
- package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -2
- package/dist/apps/shared/components/calendar/EventFormParts.js +5 -16
- package/dist/apps/shared/components/calendar/GuestInput.d.ts +45 -0
- package/dist/apps/shared/components/calendar/GuestInput.js +30 -0
- package/dist/apps/shared/components/calendar/RequestChangeForm.js +11 -22
- package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -2
- package/dist/apps/shared/components/calendar/eventFormat.d.ts +21 -8
- package/dist/apps/shared/components/calendar/eventFormat.js +50 -23
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -2
- package/dist/apps/shared/components/contacts/ContactsSidebar.js +1 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +3 -1
- package/dist/apps/shared/components/contacts/ContactsToolbar.js +4 -2
- package/dist/apps/shared/components/contacts/layout/ContactsShell.js +4 -3
- package/dist/apps/shared/components/layout/FloatingActionButton.d.ts +13 -0
- package/dist/apps/shared/components/layout/FloatingActionButton.js +9 -0
- package/dist/apps/shared/components/layout/UserMenu.d.ts +4 -1
- package/dist/apps/shared/components/layout/UserMenu.js +6 -5
- package/dist/apps/shared/components/mail/ConversationThreadPane.js +127 -22
- package/dist/apps/shared/components/mail/MailSelectionBar.d.ts +6 -1
- package/dist/apps/shared/components/mail/MailSelectionBar.js +3 -3
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +8 -4
- package/dist/apps/shared/components/mail/compose/RecipientInput.d.ts +21 -1
- package/dist/apps/shared/components/mail/compose/RecipientInput.js +18 -10
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.d.ts +6 -0
- package/dist/apps/shared/components/mail/layout/FolderBadgeChip.js +9 -0
- package/dist/apps/shared/components/mail/layout/MailShell.d.ts +5 -0
- package/dist/apps/shared/components/mail/layout/MailShell.js +64 -45
- package/dist/apps/shared/components/mail/layout/SidebarSection.d.ts +23 -0
- package/dist/apps/shared/components/mail/layout/SidebarSection.js +16 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.d.ts +14 -0
- package/dist/apps/shared/components/mail/reading/PendingMessageCard.js +16 -0
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +6 -5
- package/dist/apps/shared/components/tasks/TasksSidebar.js +1 -1
- package/dist/apps/shared/components/tasks/layout/TasksShell.js +4 -3
- package/dist/apps/shared/mail/folderCounts.d.ts +10 -0
- package/dist/apps/shared/mail/folderCounts.js +12 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.d.ts +80 -0
- package/dist/apps/shared/mail/outbox/outgoingReplies.js +131 -0
- package/dist/apps/shared/mail/outbox/sendJob.d.ts +5 -1
- package/dist/apps/shared/mail/outbox/sendJob.js +14 -1
- package/dist/apps/shared/mail/outbox/sendOutcomes.js +13 -4
- package/dist/apps/shared/mail/primaryMailbox.d.ts +29 -0
- package/dist/apps/shared/mail/primaryMailbox.js +61 -0
- package/dist/apps/shared/mail/useCollapsedSections.d.ts +32 -0
- package/dist/apps/shared/mail/useCollapsedSections.js +110 -0
- package/dist/apps/shared/mail/useMailConnection.js +3 -1
- package/dist/apps/shared/search/crossMailboxSearch.d.ts +72 -0
- package/dist/apps/shared/search/crossMailboxSearch.js +136 -0
- package/dist/apps/shared/styles/app.css +396 -386
- package/dist/apps/www/calendar/index.js +3 -2
- package/dist/apps/www/contacts/index.js +3 -1
- package/dist/apps/www/index.js +426 -189
- package/package.json +2 -2
|
@@ -1,277 +1,277 @@
|
|
|
1
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
-
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
-
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
-
import { getPushClient, PushEvent } from "@rapidmx/react-shared/mail/pushClient.js";
|
|
8
|
-
import { SendEvent, parseSendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
|
|
9
|
-
import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
|
|
10
|
-
import { SIGN_OUT_CHANNEL } from "../search/localIndexRpcClient.js";
|
|
11
|
-
import { FolderCounts, useFolderCounts } from "./folderCounts.js";
|
|
12
|
-
import { FolderChange, sortFolders } from "./folderTree.js";
|
|
13
|
-
|
|
14
|
-
/** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
|
|
15
|
-
* that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
|
|
16
|
-
export const LIVE_POLL_INTERVAL_MS = 45_000;
|
|
17
|
-
|
|
18
|
-
/** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
|
|
19
|
-
export const LIVE_EVENT_DEBOUNCE_MS = 400;
|
|
20
|
-
|
|
21
|
-
/** What changed, as far as a list on screen is concerned. */
|
|
22
|
-
export interface LiveUpdates {
|
|
23
|
-
/** Bumped each time the list should quietly refetch its first page. `0` until the first bump. */
|
|
24
|
-
tick: number;
|
|
25
|
-
/** The folders the events behind the latest bump touched - `null` when that isn't known (a poll, a reconnect, the tab
|
|
26
|
-
* coming back to the front), meaning any folder may have changed. */
|
|
27
|
-
folderUids: ReadonlySet<string> | null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const NO_LIVE_UPDATES: LiveUpdates = { tick: 0, folderUids: null };
|
|
31
|
-
|
|
32
|
-
/** The class names `NotificationUtils.sendMessage()` publishes are the model's (`MessageMongo`, `MessageSQL`, `FolderMongo`). */
|
|
33
|
-
const MESSAGE_EVENT = /^Message/;
|
|
34
|
-
const FOLDER_EVENT = /^Folder/;
|
|
35
|
-
const MESSAGE_ACTIONS = new Set(["create", "update", "delete"]);
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
|
|
39
|
-
* mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
|
|
40
|
-
* they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
|
|
41
|
-
* just appeared is filed by its type like any other, so when the server's cap bites it is the last of them that go without live
|
|
42
|
-
* updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
|
|
43
|
-
*/
|
|
44
|
-
export function pushChannelsFor(mailboxFolders: MailboxFolders[], mailboxes: Mailbox[]): string[] {
|
|
45
|
-
const inboxes = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type === "inbox"));
|
|
46
|
-
const others = mailboxFolders.flatMap((entry) => sortFolders(entry.folders.filter((folder) => folder.type !== "inbox")));
|
|
47
|
-
return [...new Set([...inboxes.map((folder) => folder.uid), ...mailboxes.map((mailbox) => mailbox.uid), ...others.map((folder) => folder.uid)])];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isFolder(value: unknown): value is Folder {
|
|
51
|
-
const folder = value as Partial<Folder> | null;
|
|
52
|
-
return !!folder && typeof folder.uid === "string" && typeof folder.mailboxUid === "string" && typeof folder.type === "string";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** A `Folder` update event that says the folder's name or type changed (and names the folder): the counts-only ones the server publishes after every message change do not. */
|
|
56
|
-
function isFolderChange(value: unknown): value is FolderChange {
|
|
57
|
-
const change = value as Partial<FolderChange> | null;
|
|
58
|
-
return !!change && typeof change.uid === "string" && (typeof change.name === "string" || typeof change.type === "string");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isMessage(value: unknown): value is Message {
|
|
62
|
-
const message = value as Partial<Message> | null;
|
|
63
|
-
return (
|
|
64
|
-
!!message &&
|
|
65
|
-
typeof message.uid === "string" &&
|
|
66
|
-
typeof message.folderUid === "string" &&
|
|
67
|
-
typeof message.flags === "object" &&
|
|
68
|
-
message.flags !== null
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** The folder a message event is about: the message's own `folderUid`, else the channel it arrived on (when the server said). */
|
|
73
|
-
function folderOfMessageEvent(event: PushEvent): string | undefined {
|
|
74
|
-
const data = event.data as { folderUid?: unknown } | null | undefined;
|
|
75
|
-
if (typeof data?.folderUid === "string") {
|
|
76
|
-
return data.folderUid;
|
|
77
|
-
}
|
|
78
|
-
return event.channel;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export interface UseMailLiveUpdatesOptions {
|
|
82
|
-
/** Nothing runs without a signed-in user. */
|
|
83
|
-
userUid?: string;
|
|
84
|
-
mailboxes: Mailbox[];
|
|
85
|
-
mailboxFolders: MailboxFolders[];
|
|
86
|
-
/** Called with a folder another client (or this one) created, so the sidebar shows it. Called only for one not already known. */
|
|
87
|
-
onFolderCreated: (folder: Folder) => void;
|
|
88
|
-
/** Called when a folder's name or type was changed elsewhere (a `Folder` update event that says so - one that only carries counts is not this). */
|
|
89
|
-
onFolderChanged?: (change: FolderChange) => void;
|
|
90
|
-
/** Called with the uid of a folder that was deleted elsewhere. */
|
|
91
|
-
onFolderDeleted?: (folderUid: string) => void;
|
|
92
|
-
/** Called with a mailbox's folders as the server just listed them - on every refresh (a message event, the poll, a reconnect, the tab
|
|
93
|
-
* coming back, a send) - so a folder the tree does not know is found whichever way it appeared. */
|
|
94
|
-
onFoldersListed?: (folders: Folder[]) => void;
|
|
95
|
-
/** Called with each new message the push connection announces (a `create` event) - once per message, never for a list
|
|
96
|
-
* refetch, the initial load or a reconnect (nothing published while the socket was down is replayed). The caller decides
|
|
97
|
-
* whether it is worth telling the user about. */
|
|
98
|
-
onMessageCreated?: (message: Message) => void;
|
|
99
|
-
/** Called with the outcome of a message sent in the background (`send-succeeded`, `send-failed`, `send-retrying`). Every one also
|
|
100
|
-
* refreshes what is on screen (the Outbox and Sent Items lists and counts change with it). */
|
|
101
|
-
onSendEvent?: (event: SendEvent) => void;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface MailLiveUpdates {
|
|
105
|
-
live: LiveUpdates;
|
|
106
|
-
/** The folder badges' numbers and how to change them - see `useFolderCounts()`. Kept in an overlay of their own, **not** in
|
|
107
|
-
* `mailboxFolders`, because the list effect reloads - and forgets the selection - whenever `mailboxFolders` changes. */
|
|
108
|
-
folderCounts: FolderCounts;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Keeps Mail current without a page reload. One shared push connection per tab (`getPushClient()`) is subscribed to every
|
|
113
|
-
* folder of every accessible mailbox; a message event for any of them, a reconnect, a poll of the safety net (every
|
|
114
|
-
* `LIVE_POLL_INTERVAL_MS` while the tab is visible - and the only mechanism, silently, where the socket can't connect), the
|
|
115
|
-
* tab coming back to the front or the browser coming back online, all end in the same debounced refresh: `live` is bumped
|
|
116
|
-
* (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's counts are re-read. A new message
|
|
117
|
-
* also bumps its folder's badge at once and is announced to `onMessageCreated`, and the server's `Folder` update events (the
|
|
118
|
-
* folder's real counts) are applied to the badges - see `useFolderCounts()`.
|
|
119
|
-
*
|
|
120
|
-
* Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
|
|
121
|
-
* Renders nothing and does nothing where there is no window (server-side rendering).
|
|
122
|
-
*/
|
|
123
|
-
export function useMailLiveUpdates({
|
|
124
|
-
userUid,
|
|
125
|
-
mailboxes,
|
|
126
|
-
mailboxFolders,
|
|
127
|
-
onFolderCreated,
|
|
128
|
-
onFolderChanged,
|
|
129
|
-
onFolderDeleted,
|
|
130
|
-
onFoldersListed,
|
|
131
|
-
onMessageCreated,
|
|
132
|
-
onSendEvent,
|
|
133
|
-
}: UseMailLiveUpdatesOptions): MailLiveUpdates {
|
|
134
|
-
const [live, setLive] = useState<LiveUpdates>(NO_LIVE_UPDATES);
|
|
135
|
-
const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
|
|
136
|
-
const folderCounts = useFolderCounts(mailboxes, folders, onFoldersListed);
|
|
137
|
-
// The latest inputs, for the long-lived listeners below.
|
|
138
|
-
const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts });
|
|
139
|
-
latestRef.current = { mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts };
|
|
140
|
-
|
|
141
|
-
const channels = useMemo(() => pushChannelsFor(mailboxFolders, mailboxes), [mailboxFolders, mailboxes]);
|
|
142
|
-
const channelKey = channels.join("|");
|
|
143
|
-
const channelsRef = useRef(channels);
|
|
144
|
-
channelsRef.current = channels;
|
|
145
|
-
|
|
146
|
-
useEffect(() => {
|
|
147
|
-
if (!userUid) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const client = getPushClient();
|
|
151
|
-
let stopped = false;
|
|
152
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
153
|
-
let pendingFolders = new Set<string>();
|
|
154
|
-
let pendingUnknown = false;
|
|
155
|
-
let everOpen = false;
|
|
156
|
-
|
|
157
|
-
// Never runs once `stopped`: whatever sets it clears the timer first.
|
|
158
|
-
function flush() {
|
|
159
|
-
timer = undefined;
|
|
160
|
-
const folderUids = pendingUnknown ? null : new Set(pendingFolders);
|
|
161
|
-
pendingFolders = new Set();
|
|
162
|
-
pendingUnknown = false;
|
|
163
|
-
setLive((previous) => ({ tick: previous.tick + 1, folderUids }));
|
|
164
|
-
latestRef.current.folderCounts.refresh(0);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Asks for a refresh: of the given folders, or (no argument) of whatever may have changed. */
|
|
168
|
-
function schedule(folderUid?: string) {
|
|
169
|
-
if (stopped) {
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (folderUid) {
|
|
173
|
-
pendingFolders.add(folderUid);
|
|
174
|
-
} else {
|
|
175
|
-
pendingUnknown = true;
|
|
176
|
-
}
|
|
177
|
-
timer ??= setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const offEvent = client.onEvent((event) => {
|
|
181
|
-
const sendEvent = parseSendEvent(event);
|
|
182
|
-
if (sendEvent) {
|
|
183
|
-
// A queued message was relayed (or failed to be): it left Outbox for Sent Items, or stayed there failed - every list and count may have changed.
|
|
184
|
-
latestRef.current.onSendEvent?.(sendEvent);
|
|
185
|
-
schedule();
|
|
186
|
-
} else if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
|
|
187
|
-
if (event.action === "create" && isMessage(event.data)) {
|
|
188
|
-
// Once per message, whatever else the event triggers: the badge first, then whoever announces it.
|
|
189
|
-
if (latestRef.current.folderCounts.noteCreated(event.data)) {
|
|
190
|
-
latestRef.current.onMessageCreated?.(event.data);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
schedule(folderOfMessageEvent(event));
|
|
194
|
-
} else if (FOLDER_EVENT.test(event.type) && event.action === "create" && isFolder(event.data)) {
|
|
195
|
-
// A folder made by the server (on first use, or by another client) or by this one: filed in the sidebar and subscribed to at once (the
|
|
196
|
-
// channel list follows the tree), and its real counts read - all without a reload.
|
|
197
|
-
const folder = event.data;
|
|
198
|
-
const known = latestRef.current.mailboxFolders.some((entry) => entry.folders.some((f) => f.uid === folder.uid));
|
|
199
|
-
if (!known) {
|
|
200
|
-
latestRef.current.onFolderCreated(folder);
|
|
201
|
-
latestRef.current.folderCounts.refresh(0);
|
|
202
|
-
}
|
|
203
|
-
} else if (FOLDER_EVENT.test(event.type) && event.action === "update") {
|
|
204
|
-
if (isFolderChange(event.data)) {
|
|
205
|
-
latestRef.current.onFolderChanged?.(event.data);
|
|
206
|
-
}
|
|
207
|
-
latestRef.current.folderCounts.applyFolderEvent(event.data);
|
|
208
|
-
} else if (FOLDER_EVENT.test(event.type) && event.action === "delete") {
|
|
209
|
-
const uid = (event.data as { uid?: unknown } | null | undefined)?.uid;
|
|
210
|
-
if (typeof uid === "string") {
|
|
211
|
-
latestRef.current.onFolderDeleted?.(uid);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
// Anything published while the socket was down is gone for good, so every reconnect is followed by a refresh.
|
|
216
|
-
const offStatus = client.onStatus((status) => {
|
|
217
|
-
if (status === "open") {
|
|
218
|
-
if (everOpen) {
|
|
219
|
-
schedule();
|
|
220
|
-
}
|
|
221
|
-
everOpen = true;
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const poll = setInterval(() => {
|
|
226
|
-
if (document.visibilityState === "visible") {
|
|
227
|
-
schedule();
|
|
228
|
-
}
|
|
229
|
-
}, LIVE_POLL_INTERVAL_MS);
|
|
230
|
-
function refreshNow() {
|
|
231
|
-
if (document.visibilityState === "visible") {
|
|
232
|
-
client.reconnectNow();
|
|
233
|
-
schedule();
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
function handleOnline() {
|
|
237
|
-
client.reconnectNow();
|
|
238
|
-
schedule();
|
|
239
|
-
}
|
|
240
|
-
document.addEventListener("visibilitychange", refreshNow);
|
|
241
|
-
window.addEventListener("focus", refreshNow);
|
|
242
|
-
window.addEventListener("online", handleOnline);
|
|
243
|
-
|
|
244
|
-
// A sign-out, in this tab or another, ended the session behind the socket: close it for good and stop refreshing.
|
|
245
|
-
const signOut = typeof BroadcastChannel === "undefined" ? undefined : new BroadcastChannel(SIGN_OUT_CHANNEL);
|
|
246
|
-
signOut?.addEventListener("message", (event: MessageEvent<{ type?: string }>) => {
|
|
247
|
-
if (event.data?.type === "sign-out") {
|
|
248
|
-
stopped = true;
|
|
249
|
-
clearTimeout(timer);
|
|
250
|
-
client.close();
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
client.setChannels(channelsRef.current);
|
|
255
|
-
client.start();
|
|
256
|
-
|
|
257
|
-
return () => {
|
|
258
|
-
stopped = true;
|
|
259
|
-
clearTimeout(timer);
|
|
260
|
-
clearInterval(poll);
|
|
261
|
-
offEvent();
|
|
262
|
-
offStatus();
|
|
263
|
-
document.removeEventListener("visibilitychange", refreshNow);
|
|
264
|
-
window.removeEventListener("focus", refreshNow);
|
|
265
|
-
window.removeEventListener("online", handleOnline);
|
|
266
|
-
signOut?.close();
|
|
267
|
-
};
|
|
268
|
-
}, [userUid]);
|
|
269
|
-
|
|
270
|
-
useEffect(() => {
|
|
271
|
-
if (userUid) {
|
|
272
|
-
getPushClient().setChannels(channels);
|
|
273
|
-
}
|
|
274
|
-
}, [userUid, channelKey]);
|
|
275
|
-
|
|
276
|
-
return { live, folderCounts };
|
|
277
|
-
}
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2026 Jean-Philippe Steinmetz
|
|
3
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
5
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { Folder, Mailbox, Message } from "@rapidmx/react-shared/mail/mailApi.js";
|
|
7
|
+
import { getPushClient, PushEvent } from "@rapidmx/react-shared/mail/pushClient.js";
|
|
8
|
+
import { SendEvent, parseSendEvent } from "@rapidmx/react-shared/mail/sendEvents.js";
|
|
9
|
+
import type { MailboxFolders } from "../components/mail/layout/MailShell.js";
|
|
10
|
+
import { SIGN_OUT_CHANNEL } from "../search/localIndexRpcClient.js";
|
|
11
|
+
import { FolderCounts, useFolderCounts } from "./folderCounts.js";
|
|
12
|
+
import { FolderChange, sortFolders } from "./folderTree.js";
|
|
13
|
+
|
|
14
|
+
/** How often the safety-net poll runs while the tab is visible. The push socket does the real work; this is for a network
|
|
15
|
+
* that blocks or silently drops it, and for events published while it was reconnecting (Redis pub/sub is never replayed). */
|
|
16
|
+
export const LIVE_POLL_INTERVAL_MS = 45_000;
|
|
17
|
+
|
|
18
|
+
/** Events arriving this close together are answered with one refresh - a burst of new mail is one refetch, not one each. */
|
|
19
|
+
export const LIVE_EVENT_DEBOUNCE_MS = 400;
|
|
20
|
+
|
|
21
|
+
/** What changed, as far as a list on screen is concerned. */
|
|
22
|
+
export interface LiveUpdates {
|
|
23
|
+
/** Bumped each time the list should quietly refetch its first page. `0` until the first bump. */
|
|
24
|
+
tick: number;
|
|
25
|
+
/** The folders the events behind the latest bump touched - `null` when that isn't known (a poll, a reconnect, the tab
|
|
26
|
+
* coming back to the front), meaning any folder may have changed. */
|
|
27
|
+
folderUids: ReadonlySet<string> | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const NO_LIVE_UPDATES: LiveUpdates = { tick: 0, folderUids: null };
|
|
31
|
+
|
|
32
|
+
/** The class names `NotificationUtils.sendMessage()` publishes are the model's (`MessageMongo`, `MessageSQL`, `FolderMongo`). */
|
|
33
|
+
const MESSAGE_EVENT = /^Message/;
|
|
34
|
+
const FOLDER_EVENT = /^Folder/;
|
|
35
|
+
const MESSAGE_ACTIONS = new Set(["create", "update", "delete"]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The channels to subscribe to, most important first (the push client keeps the first `PUSH_MAX_CHANNELS`): every
|
|
39
|
+
* mailbox's Inbox - where mail lands - then the mailbox uids themselves (which announce new, renamed and deleted folders, so
|
|
40
|
+
* they must outrank any folder's own mail), then each mailbox's other folders in the sidebar's own order - a folder that has
|
|
41
|
+
* just appeared is filed by its type like any other, so when the server's cap bites it is the last of them that go without live
|
|
42
|
+
* updates. A folder is a channel of its own; subscribing to a mailbox does not deliver its folders' events.
|
|
43
|
+
*/
|
|
44
|
+
export function pushChannelsFor(mailboxFolders: MailboxFolders[], mailboxes: Mailbox[]): string[] {
|
|
45
|
+
const inboxes = mailboxFolders.flatMap((entry) => entry.folders.filter((folder) => folder.type === "inbox"));
|
|
46
|
+
const others = mailboxFolders.flatMap((entry) => sortFolders(entry.folders.filter((folder) => folder.type !== "inbox")));
|
|
47
|
+
return [...new Set([...inboxes.map((folder) => folder.uid), ...mailboxes.map((mailbox) => mailbox.uid), ...others.map((folder) => folder.uid)])];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isFolder(value: unknown): value is Folder {
|
|
51
|
+
const folder = value as Partial<Folder> | null;
|
|
52
|
+
return !!folder && typeof folder.uid === "string" && typeof folder.mailboxUid === "string" && typeof folder.type === "string";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A `Folder` update event that says the folder's name or type changed (and names the folder): the counts-only ones the server publishes after every message change do not. */
|
|
56
|
+
function isFolderChange(value: unknown): value is FolderChange {
|
|
57
|
+
const change = value as Partial<FolderChange> | null;
|
|
58
|
+
return !!change && typeof change.uid === "string" && (typeof change.name === "string" || typeof change.type === "string");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isMessage(value: unknown): value is Message {
|
|
62
|
+
const message = value as Partial<Message> | null;
|
|
63
|
+
return (
|
|
64
|
+
!!message &&
|
|
65
|
+
typeof message.uid === "string" &&
|
|
66
|
+
typeof message.folderUid === "string" &&
|
|
67
|
+
typeof message.flags === "object" &&
|
|
68
|
+
message.flags !== null
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The folder a message event is about: the message's own `folderUid`, else the channel it arrived on (when the server said). */
|
|
73
|
+
function folderOfMessageEvent(event: PushEvent): string | undefined {
|
|
74
|
+
const data = event.data as { folderUid?: unknown } | null | undefined;
|
|
75
|
+
if (typeof data?.folderUid === "string") {
|
|
76
|
+
return data.folderUid;
|
|
77
|
+
}
|
|
78
|
+
return event.channel;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface UseMailLiveUpdatesOptions {
|
|
82
|
+
/** Nothing runs without a signed-in user. */
|
|
83
|
+
userUid?: string;
|
|
84
|
+
mailboxes: Mailbox[];
|
|
85
|
+
mailboxFolders: MailboxFolders[];
|
|
86
|
+
/** Called with a folder another client (or this one) created, so the sidebar shows it. Called only for one not already known. */
|
|
87
|
+
onFolderCreated: (folder: Folder) => void;
|
|
88
|
+
/** Called when a folder's name or type was changed elsewhere (a `Folder` update event that says so - one that only carries counts is not this). */
|
|
89
|
+
onFolderChanged?: (change: FolderChange) => void;
|
|
90
|
+
/** Called with the uid of a folder that was deleted elsewhere. */
|
|
91
|
+
onFolderDeleted?: (folderUid: string) => void;
|
|
92
|
+
/** Called with a mailbox's folders as the server just listed them - on every refresh (a message event, the poll, a reconnect, the tab
|
|
93
|
+
* coming back, a send) - so a folder the tree does not know is found whichever way it appeared. */
|
|
94
|
+
onFoldersListed?: (folders: Folder[]) => void;
|
|
95
|
+
/** Called with each new message the push connection announces (a `create` event) - once per message, never for a list
|
|
96
|
+
* refetch, the initial load or a reconnect (nothing published while the socket was down is replayed). The caller decides
|
|
97
|
+
* whether it is worth telling the user about. */
|
|
98
|
+
onMessageCreated?: (message: Message) => void;
|
|
99
|
+
/** Called with the outcome of a message sent in the background (`send-succeeded`, `send-failed`, `send-retrying`). Every one also
|
|
100
|
+
* refreshes what is on screen (the Outbox and Sent Items lists and counts change with it). */
|
|
101
|
+
onSendEvent?: (event: SendEvent) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface MailLiveUpdates {
|
|
105
|
+
live: LiveUpdates;
|
|
106
|
+
/** The folder badges' numbers and how to change them - see `useFolderCounts()`. Kept in an overlay of their own, **not** in
|
|
107
|
+
* `mailboxFolders`, because the list effect reloads - and forgets the selection - whenever `mailboxFolders` changes. */
|
|
108
|
+
folderCounts: FolderCounts;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Keeps Mail current without a page reload. One shared push connection per tab (`getPushClient()`) is subscribed to every
|
|
113
|
+
* folder of every accessible mailbox; a message event for any of them, a reconnect, a poll of the safety net (every
|
|
114
|
+
* `LIVE_POLL_INTERVAL_MS` while the tab is visible - and the only mechanism, silently, where the socket can't connect), the
|
|
115
|
+
* tab coming back to the front or the browser coming back online, all end in the same debounced refresh: `live` is bumped
|
|
116
|
+
* (the list on screen refetches its first page - see `mergeFirstPage()`) and every folder's counts are re-read. A new message
|
|
117
|
+
* also bumps its folder's badge at once and is announced to `onMessageCreated`, and the server's `Folder` update events (the
|
|
118
|
+
* folder's real counts) are applied to the badges - see `useFolderCounts()`.
|
|
119
|
+
*
|
|
120
|
+
* Sign-out closes the socket for good: another tab's or this one's, heard on the same channel `AppShell` listens on.
|
|
121
|
+
* Renders nothing and does nothing where there is no window (server-side rendering).
|
|
122
|
+
*/
|
|
123
|
+
export function useMailLiveUpdates({
|
|
124
|
+
userUid,
|
|
125
|
+
mailboxes,
|
|
126
|
+
mailboxFolders,
|
|
127
|
+
onFolderCreated,
|
|
128
|
+
onFolderChanged,
|
|
129
|
+
onFolderDeleted,
|
|
130
|
+
onFoldersListed,
|
|
131
|
+
onMessageCreated,
|
|
132
|
+
onSendEvent,
|
|
133
|
+
}: UseMailLiveUpdatesOptions): MailLiveUpdates {
|
|
134
|
+
const [live, setLive] = useState<LiveUpdates>(NO_LIVE_UPDATES);
|
|
135
|
+
const folders = useMemo(() => mailboxFolders.flatMap((entry) => entry.folders), [mailboxFolders]);
|
|
136
|
+
const folderCounts = useFolderCounts(mailboxes, folders, onFoldersListed);
|
|
137
|
+
// The latest inputs, for the long-lived listeners below.
|
|
138
|
+
const latestRef = useRef({ mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts });
|
|
139
|
+
latestRef.current = { mailboxes, mailboxFolders, onFolderCreated, onFolderChanged, onFolderDeleted, onMessageCreated, onSendEvent, folderCounts };
|
|
140
|
+
|
|
141
|
+
const channels = useMemo(() => pushChannelsFor(mailboxFolders, mailboxes), [mailboxFolders, mailboxes]);
|
|
142
|
+
const channelKey = channels.join("|");
|
|
143
|
+
const channelsRef = useRef(channels);
|
|
144
|
+
channelsRef.current = channels;
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (!userUid) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const client = getPushClient();
|
|
151
|
+
let stopped = false;
|
|
152
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
153
|
+
let pendingFolders = new Set<string>();
|
|
154
|
+
let pendingUnknown = false;
|
|
155
|
+
let everOpen = false;
|
|
156
|
+
|
|
157
|
+
// Never runs once `stopped`: whatever sets it clears the timer first.
|
|
158
|
+
function flush() {
|
|
159
|
+
timer = undefined;
|
|
160
|
+
const folderUids = pendingUnknown ? null : new Set(pendingFolders);
|
|
161
|
+
pendingFolders = new Set();
|
|
162
|
+
pendingUnknown = false;
|
|
163
|
+
setLive((previous) => ({ tick: previous.tick + 1, folderUids }));
|
|
164
|
+
latestRef.current.folderCounts.refresh(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Asks for a refresh: of the given folders, or (no argument) of whatever may have changed. */
|
|
168
|
+
function schedule(folderUid?: string) {
|
|
169
|
+
if (stopped) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (folderUid) {
|
|
173
|
+
pendingFolders.add(folderUid);
|
|
174
|
+
} else {
|
|
175
|
+
pendingUnknown = true;
|
|
176
|
+
}
|
|
177
|
+
timer ??= setTimeout(flush, LIVE_EVENT_DEBOUNCE_MS);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const offEvent = client.onEvent((event) => {
|
|
181
|
+
const sendEvent = parseSendEvent(event);
|
|
182
|
+
if (sendEvent) {
|
|
183
|
+
// A queued message was relayed (or failed to be): it left Outbox for Sent Items, or stayed there failed - every list and count may have changed.
|
|
184
|
+
latestRef.current.onSendEvent?.(sendEvent);
|
|
185
|
+
schedule();
|
|
186
|
+
} else if (MESSAGE_EVENT.test(event.type) && MESSAGE_ACTIONS.has(event.action ?? "")) {
|
|
187
|
+
if (event.action === "create" && isMessage(event.data)) {
|
|
188
|
+
// Once per message, whatever else the event triggers: the badge first, then whoever announces it.
|
|
189
|
+
if (latestRef.current.folderCounts.noteCreated(event.data)) {
|
|
190
|
+
latestRef.current.onMessageCreated?.(event.data);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
schedule(folderOfMessageEvent(event));
|
|
194
|
+
} else if (FOLDER_EVENT.test(event.type) && event.action === "create" && isFolder(event.data)) {
|
|
195
|
+
// A folder made by the server (on first use, or by another client) or by this one: filed in the sidebar and subscribed to at once (the
|
|
196
|
+
// channel list follows the tree), and its real counts read - all without a reload.
|
|
197
|
+
const folder = event.data;
|
|
198
|
+
const known = latestRef.current.mailboxFolders.some((entry) => entry.folders.some((f) => f.uid === folder.uid));
|
|
199
|
+
if (!known) {
|
|
200
|
+
latestRef.current.onFolderCreated(folder);
|
|
201
|
+
latestRef.current.folderCounts.refresh(0);
|
|
202
|
+
}
|
|
203
|
+
} else if (FOLDER_EVENT.test(event.type) && event.action === "update") {
|
|
204
|
+
if (isFolderChange(event.data)) {
|
|
205
|
+
latestRef.current.onFolderChanged?.(event.data);
|
|
206
|
+
}
|
|
207
|
+
latestRef.current.folderCounts.applyFolderEvent(event.data);
|
|
208
|
+
} else if (FOLDER_EVENT.test(event.type) && event.action === "delete") {
|
|
209
|
+
const uid = (event.data as { uid?: unknown } | null | undefined)?.uid;
|
|
210
|
+
if (typeof uid === "string") {
|
|
211
|
+
latestRef.current.onFolderDeleted?.(uid);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
// Anything published while the socket was down is gone for good, so every reconnect is followed by a refresh.
|
|
216
|
+
const offStatus = client.onStatus((status) => {
|
|
217
|
+
if (status === "open") {
|
|
218
|
+
if (everOpen) {
|
|
219
|
+
schedule();
|
|
220
|
+
}
|
|
221
|
+
everOpen = true;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const poll = setInterval(() => {
|
|
226
|
+
if (document.visibilityState === "visible") {
|
|
227
|
+
schedule();
|
|
228
|
+
}
|
|
229
|
+
}, LIVE_POLL_INTERVAL_MS);
|
|
230
|
+
function refreshNow() {
|
|
231
|
+
if (document.visibilityState === "visible") {
|
|
232
|
+
client.reconnectNow();
|
|
233
|
+
schedule();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function handleOnline() {
|
|
237
|
+
client.reconnectNow();
|
|
238
|
+
schedule();
|
|
239
|
+
}
|
|
240
|
+
document.addEventListener("visibilitychange", refreshNow);
|
|
241
|
+
window.addEventListener("focus", refreshNow);
|
|
242
|
+
window.addEventListener("online", handleOnline);
|
|
243
|
+
|
|
244
|
+
// A sign-out, in this tab or another, ended the session behind the socket: close it for good and stop refreshing.
|
|
245
|
+
const signOut = typeof BroadcastChannel === "undefined" ? undefined : new BroadcastChannel(SIGN_OUT_CHANNEL);
|
|
246
|
+
signOut?.addEventListener("message", (event: MessageEvent<{ type?: string }>) => {
|
|
247
|
+
if (event.data?.type === "sign-out") {
|
|
248
|
+
stopped = true;
|
|
249
|
+
clearTimeout(timer);
|
|
250
|
+
client.close();
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
client.setChannels(channelsRef.current);
|
|
255
|
+
client.start();
|
|
256
|
+
|
|
257
|
+
return () => {
|
|
258
|
+
stopped = true;
|
|
259
|
+
clearTimeout(timer);
|
|
260
|
+
clearInterval(poll);
|
|
261
|
+
offEvent();
|
|
262
|
+
offStatus();
|
|
263
|
+
document.removeEventListener("visibilitychange", refreshNow);
|
|
264
|
+
window.removeEventListener("focus", refreshNow);
|
|
265
|
+
window.removeEventListener("online", handleOnline);
|
|
266
|
+
signOut?.close();
|
|
267
|
+
};
|
|
268
|
+
}, [userUid]);
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
if (userUid) {
|
|
272
|
+
getPushClient().setChannels(channels);
|
|
273
|
+
}
|
|
274
|
+
}, [userUid, channelKey]);
|
|
275
|
+
|
|
276
|
+
return { live, folderCounts };
|
|
277
|
+
}
|