@arbidocs/react 0.5.89 → 0.5.91

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 CHANGED
@@ -258,9 +258,14 @@ cheapest way back: it clears "Work offline", says there is no network, or reload
258
258
 
259
259
  ## Offline mail and calendar
260
260
 
261
- Mail can be kept on the device, sealed at rest to the signed-in user's key, bound to that account
262
- and readable only by them: offline reading and search, an Outbox whose sends go out exactly once
263
- on reconnect (each keeps its `request_id`), and drafts written offline. A host turns it on with
261
+ Mail is kept on the device, sealed at rest to the signed-in user's key, bound to that account
262
+ and readable only by them: reading and searching recent mail offline (the last 7 days by default;
263
+ per device, Off / 1 / 7 / 30 / 90 days, `useOfflineMailSettings`), an Outbox whose sends go out
264
+ exactly once and in order on reconnect (each keeps its `request_id`), drafts written offline, and
265
+ changes made offline (read, moved, Trash, deleted: `queueMailChange`, `usePendingMailChanges`),
266
+ applied in order on reconnect. Nothing goes out under "Work offline". The default mail client (no
267
+ host `mail`) already reads through `offlineMailReads`, and `defaultHost` in `@arbidocs/blocks`
268
+ mounts the runtime and wipes the mail at sign-out. A host with its own client and session adds
264
269
  three pieces:
265
270
 
266
271
  ```tsx
@@ -269,12 +274,12 @@ import { createMailClient, offlineMailReads, useOfflineMailRuntime, wipeOfflineM
269
274
  const client = createMailClient((path, init) => getArbi().mail.fetch(path, init), {
270
275
  ...offlineMailReads(getArbi), // reads answer from the device offline
271
276
  })
272
- useOfflineMailRuntime({ openOutbox }) // mounted once: sends the Outbox, uploads drafts, syncs
277
+ useOfflineMailRuntime({ openOutbox }) // mounted once: sends the Outbox, applies changes, syncs
273
278
  await wipeOfflineMail() // at sign-out
274
279
  ```
275
280
 
276
- The mailbox kit's device storage is `offlineMailSupport` (`@arbidocs/blocks/connect`); the
277
- per-device settings are `ConnectedMailOfflineSettings`.
281
+ The mail kits use `offlineMailSupport` (`@arbidocs/blocks/connect`) as their device storage by
282
+ default; the per-device settings are `ConnectedMailOfflineSettings`.
278
283
 
279
284
  The calendar is kept the same way (its own sealed database): reads are answered from the device
280
285
  offline, and event writes made offline wait in an outbox that is replayed in order once back online.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkEOKCXEYN_cjs = require('../../chunk-EOKCXEYN.cjs');
3
+ var chunkX4HGUBWU_cjs = require('../../chunk-X4HGUBWU.cjs');
4
4
  require('../../chunk-7RUVJZZG.cjs');
5
5
  var chunkBRFRQWAY_cjs = require('../../chunk-BRFRQWAY.cjs');
6
6
  var chunkRLSBRZ6Z_cjs = require('../../chunk-RLSBRZ6Z.cjs');
@@ -123,8 +123,8 @@ function useCallTranscriber({
123
123
  const arbi = chunkBRFRQWAY_cjs.useArbi();
124
124
  const queryClient = reactQuery.useQueryClient();
125
125
  const profile = arbi.isLoggedIn ? arbi.user.profile() : null;
126
- const myProfileName = profile && chunkEOKCXEYN_cjs.toProfileView(profile).fullName || null;
127
- const deploymentAvailable = chunkEOKCXEYN_cjs.useCallTranscriptionAvailable();
126
+ const myProfileName = profile && chunkX4HGUBWU_cjs.toProfileView(profile).fullName || null;
127
+ const deploymentAvailable = chunkX4HGUBWU_cjs.useCallTranscriptionAvailable();
128
128
  const available = deploymentAvailable && !!callKey;
129
129
  const [state, setState] = react.useState(null);
130
130
  const [creatorExtId, setCreatorExtId] = react.useState(null);
@@ -167,7 +167,7 @@ function useCallTranscriber({
167
167
  };
168
168
  }, [available, callExtId]);
169
169
  react.useEffect(
170
- () => chunkEOKCXEYN_cjs.onCallEvent("call.transcription", (message) => {
170
+ () => chunkX4HGUBWU_cjs.onCallEvent("call.transcription", (message) => {
171
171
  if (message.call_ext_id === callExtId) applyState(message);
172
172
  }),
173
173
  [callExtId, applyState]
@@ -183,7 +183,7 @@ function useCallTranscriber({
183
183
  const persist = react.useCallback(
184
184
  (segment, speakerNeedsProxy) => {
185
185
  if (!callKey || persisted.current.has(segment.segmentId)) return;
186
- const role = chunkEOKCXEYN_cjs.transcriptPersistRole({
186
+ const role = chunkX4HGUBWU_cjs.transcriptPersistRole({
187
187
  myIdentity,
188
188
  canPost: canPost && (segment.speakerIdentity !== myIdentity || selfPosts.current),
189
189
  hasCallKey: true,
@@ -192,7 +192,7 @@ function useCallTranscriber({
192
192
  });
193
193
  if (!role) return;
194
194
  persisted.current.add(segment.segmentId);
195
- chunkEOKCXEYN_cjs.persistTranscriptSegment(arbi, queryClient, callExtId, callKey, segment).catch(
195
+ chunkX4HGUBWU_cjs.persistTranscriptSegment(arbi, queryClient, callExtId, callKey, segment).catch(
196
196
  (err) => {
197
197
  persisted.current.delete(segment.segmentId);
198
198
  if (role === "own" && err?.status === 403) {
@@ -212,7 +212,7 @@ function useCallTranscriber({
212
212
  );
213
213
  react.useEffect(() => {
214
214
  const onData = (payload, participant, _kind, topic, _encryption, participantIdentity) => {
215
- if (topic === chunkEOKCXEYN_cjs.CALL_TRANSCRIPTION_TOPIC) {
215
+ if (topic === chunkX4HGUBWU_cjs.CALL_TRANSCRIPTION_TOPIC) {
216
216
  if (participant || participantIdentity) return;
217
217
  try {
218
218
  const next = browser.calls.transcriptionStateFrom(JSON.parse(new TextDecoder().decode(payload)));
@@ -221,10 +221,10 @@ function useCallTranscriber({
221
221
  }
222
222
  return;
223
223
  }
224
- if (topic !== chunkEOKCXEYN_cjs.CALL_CAPTION_TOPIC || !callKey) return;
224
+ if (topic !== chunkX4HGUBWU_cjs.CALL_CAPTION_TOPIC || !callKey) return;
225
225
  const identity = participant?.identity ?? participantIdentity;
226
226
  if (!identity || identity === myIdentity) return;
227
- void chunkEOKCXEYN_cjs.decodeCaptionPacket(payload, callKey).then((packet) => {
227
+ void chunkX4HGUBWU_cjs.decodeCaptionPacket(payload, callKey).then((packet) => {
228
228
  if (!packet || packet.identity !== identity) return;
229
229
  const name = participant?.name || packet.name || identity;
230
230
  const t = Math.min(packet.t, Date.now());
@@ -264,7 +264,7 @@ function useCallTranscriber({
264
264
  })) {
265
265
  return;
266
266
  }
267
- const segmentId = chunkEOKCXEYN_cjs.newSegmentId();
267
+ const segmentId = chunkX4HGUBWU_cjs.newSegmentId();
268
268
  const startedAt = new Date(segment.startedAt).toISOString();
269
269
  let text;
270
270
  try {
@@ -298,7 +298,7 @@ function useCallTranscriber({
298
298
  });
299
299
  try {
300
300
  await localParticipant.publishData(
301
- await chunkEOKCXEYN_cjs.encodeCaptionPacket(
301
+ await chunkX4HGUBWU_cjs.encodeCaptionPacket(
302
302
  {
303
303
  v: 1,
304
304
  segment_id: segmentId,
@@ -310,7 +310,7 @@ function useCallTranscriber({
310
310
  },
311
311
  callKey
312
312
  ),
313
- { reliable: true, topic: chunkEOKCXEYN_cjs.CALL_CAPTION_TOPIC }
313
+ { reliable: true, topic: chunkX4HGUBWU_cjs.CALL_CAPTION_TOPIC }
314
314
  );
315
315
  } catch (err) {
316
316
  console.warn("[calls] could not broadcast a caption", err);
@@ -425,14 +425,14 @@ function useCallRecorder({
425
425
  };
426
426
  }, [callExtId]);
427
427
  react.useEffect(
428
- () => chunkEOKCXEYN_cjs.onCallEvent("call.recording", (message) => {
428
+ () => chunkX4HGUBWU_cjs.onCallEvent("call.recording", (message) => {
429
429
  if (message.call_ext_id === callExtId) applyState(message);
430
430
  }),
431
431
  [callExtId, applyState]
432
432
  );
433
433
  react.useEffect(() => {
434
434
  const onData = (payload, participant, _kind, topic, _encryption, participantIdentity) => {
435
- if (topic !== chunkEOKCXEYN_cjs.CALL_RECORDING_TOPIC) return;
435
+ if (topic !== chunkX4HGUBWU_cjs.CALL_RECORDING_TOPIC) return;
436
436
  if (participant || participantIdentity) return;
437
437
  try {
438
438
  const next = browser.calls.recordingStateFrom(JSON.parse(new TextDecoder().decode(payload)));
@@ -453,7 +453,7 @@ function useCallRecorder({
453
453
  try {
454
454
  const row = call ?? await arbi.calls.get(callExtId);
455
455
  if (!row.workspace_locked && !row.chat_ext_id && callKey) {
456
- const ref = await chunkEOKCXEYN_cjs.ensureMeetingChat(arbi, queryClient, callExtId, callKey);
456
+ const ref = await chunkX4HGUBWU_cjs.ensureMeetingChat(arbi, queryClient, callExtId, callKey);
457
457
  if (ref.kind === "chat") setCall({ ...row, chat_ext_id: ref.chatExtId });
458
458
  }
459
459
  const answer = await arbi.calls.recording.start(callExtId, mode);
@@ -555,7 +555,7 @@ function useTrackUploader(recording, callKey, kind, mimeType) {
555
555
  const queryClient = reactQuery.useQueryClient();
556
556
  return react.useMemo(() => {
557
557
  if (!shouldCapture || !recordingExtId || !call) return null;
558
- return chunkEOKCXEYN_cjs.trackUploaderFor({
558
+ return chunkX4HGUBWU_cjs.trackUploaderFor({
559
559
  arbi,
560
560
  callExtId: call.external_id,
561
561
  recordingExtId,
@@ -563,7 +563,7 @@ function useTrackUploader(recording, callKey, kind, mimeType) {
563
563
  mimeType,
564
564
  recordingStartedAt: startedAt,
565
565
  linkVersion,
566
- conversationKey: () => chunkEOKCXEYN_cjs.conversationKeyForCall(arbi, queryClient, call, { callKey }),
566
+ conversationKey: () => chunkX4HGUBWU_cjs.conversationKeyForCall(arbi, queryClient, call, { callKey }),
567
567
  // The server stopped the recording at the quota (its state event says so too).
568
568
  onStopped: () => markStopped()
569
569
  });
@@ -612,10 +612,10 @@ function useInCallChat({
612
612
  react.useEffect(() => {
613
613
  if (!callKey) return;
614
614
  const onData = (payload, participant, _kind, topic, _encryption, participantIdentity) => {
615
- if (topic !== chunkEOKCXEYN_cjs.CALL_CHAT_TOPIC) return;
615
+ if (topic !== chunkX4HGUBWU_cjs.CALL_CHAT_TOPIC) return;
616
616
  const identity = participant?.identity ?? participantIdentity;
617
617
  if (!identity || identity === myIdentity) return;
618
- void chunkEOKCXEYN_cjs.decodeCallChatPacket(payload, callKey).then((packet) => {
618
+ void chunkX4HGUBWU_cjs.decodeCallChatPacket(payload, callKey).then((packet) => {
619
619
  if (!packet) return;
620
620
  const key = `${identity}:${packet.id}`;
621
621
  if (seen.current.has(key)) return;
@@ -645,7 +645,7 @@ function useInCallChat({
645
645
  if (!open || !canRead || historyRequested.current) return;
646
646
  historyRequested.current = true;
647
647
  setHistoryLoading(true);
648
- chunkEOKCXEYN_cjs.loadMeetingHistory(arbi, queryClient, callExtId).then((result) => {
648
+ chunkX4HGUBWU_cjs.loadMeetingHistory(arbi, queryClient, callExtId).then((result) => {
649
649
  if (!result) return;
650
650
  setMeetingChat(result.ref);
651
651
  setHistory(result.messages);
@@ -656,13 +656,13 @@ function useInCallChat({
656
656
  }, [open, canRead, callExtId]);
657
657
  const send = react.useCallback(
658
658
  async (raw) => {
659
- const text = raw.trim().slice(0, chunkEOKCXEYN_cjs.CALL_CHAT_MAX_CHARS);
659
+ const text = raw.trim().slice(0, chunkX4HGUBWU_cjs.CALL_CHAT_MAX_CHARS);
660
660
  if (!text || !callKey || readOnly) return;
661
- const id = chunkEOKCXEYN_cjs.newPacketId();
661
+ const id = chunkX4HGUBWU_cjs.newPacketId();
662
662
  const at = Date.now();
663
663
  await localParticipant.publishData(
664
- await chunkEOKCXEYN_cjs.encodeCallChatPacket({ v: 1, id, text, at }, callKey),
665
- { reliable: true, topic: chunkEOKCXEYN_cjs.CALL_CHAT_TOPIC }
664
+ await chunkX4HGUBWU_cjs.encodeCallChatPacket({ v: 1, id, text, at }, callKey),
665
+ { reliable: true, topic: chunkX4HGUBWU_cjs.CALL_CHAT_TOPIC }
666
666
  );
667
667
  setLive((prev) => [
668
668
  ...prev,
@@ -681,7 +681,7 @@ function useInCallChat({
681
681
  (prev) => prev.map((l) => l.id === id && l.senderIdentity === myIdentity ? { ...l, status } : l)
682
682
  );
683
683
  try {
684
- setMeetingChat(await chunkEOKCXEYN_cjs.persistMeetingMessage(arbi, queryClient, callExtId, callKey, text));
684
+ setMeetingChat(await chunkX4HGUBWU_cjs.persistMeetingMessage(arbi, queryClient, callExtId, callKey, text));
685
685
  mark("saved");
686
686
  } catch (err) {
687
687
  console.warn("[calls] could not keep the message in the meeting chat", err);
@@ -693,7 +693,7 @@ function useInCallChat({
693
693
  const openMeeting = react.useCallback(async () => {
694
694
  if (!callKey) return;
695
695
  try {
696
- const ref = meetingChat ?? await chunkEOKCXEYN_cjs.ensureMeetingChat(arbi, queryClient, callExtId, callKey);
696
+ const ref = meetingChat ?? await chunkX4HGUBWU_cjs.ensureMeetingChat(arbi, queryClient, callExtId, callKey);
697
697
  setMeetingChat(ref);
698
698
  openMeetingChat?.(ref);
699
699
  } catch (err) {
@@ -701,7 +701,7 @@ function useInCallChat({
701
701
  }
702
702
  }, [callExtId, callKey, meetingChat]);
703
703
  const lines = react.useMemo(
704
- () => chunkEOKCXEYN_cjs.mergeChatLines(history, live, {
704
+ () => chunkX4HGUBWU_cjs.mergeChatLines(history, live, {
705
705
  myIdentity,
706
706
  nameOf: (identity) => identity === myIdentity ? localParticipant.name || "You" : room.remoteParticipants.get(identity)?.name || null
707
707
  }),
@@ -760,7 +760,7 @@ function useWebinar({
760
760
  const n = ++questionsRead.current;
761
761
  try {
762
762
  const rows = await webinarApi().questions.list(callExtId, linkVersion);
763
- const opened = await Promise.all(rows.map((q) => chunkEOKCXEYN_cjs.openQuestion(q, callKey)));
763
+ const opened = await Promise.all(rows.map((q) => chunkX4HGUBWU_cjs.openQuestion(q, callKey)));
764
764
  if (n === questionsRead.current) setQuestions(opened);
765
765
  } catch (err) {
766
766
  console.warn("[calls] could not load the questions", err);
@@ -772,7 +772,7 @@ function useWebinar({
772
772
  }, [refresh, refreshQuestions]);
773
773
  react.useEffect(() => {
774
774
  if (!enabled) return;
775
- return chunkEOKCXEYN_cjs.onCallEvent("call.webinar", (m) => {
775
+ return chunkX4HGUBWU_cjs.onCallEvent("call.webinar", (m) => {
776
776
  if (m.call_ext_id !== callExtId) return;
777
777
  setState((s) => s ? { ...s, live: m.live, chat: m.chat } : s);
778
778
  });
@@ -781,8 +781,8 @@ function useWebinar({
781
781
  react.useEffect(() => {
782
782
  if (!enabled) return;
783
783
  const onData = (payload, participant, _kind, topic, _encryption, participantIdentity) => {
784
- if (topic !== chunkEOKCXEYN_cjs.CALL_WEBINAR_TOPIC) return;
785
- const packet = chunkEOKCXEYN_cjs.parseWebinarPacket(payload, !!(participant || participantIdentity));
784
+ if (topic !== chunkX4HGUBWU_cjs.CALL_WEBINAR_TOPIC) return;
785
+ const packet = chunkX4HGUBWU_cjs.parseWebinarPacket(payload, !!(participant || participantIdentity));
786
786
  if (!packet) return;
787
787
  switch (packet.type) {
788
788
  case "state":
@@ -803,7 +803,7 @@ function useWebinar({
803
803
  return;
804
804
  case "demoted":
805
805
  setStageCheck(false);
806
- chunkEOKCXEYN_cjs.markRejoining(callExtId);
806
+ chunkX4HGUBWU_cjs.markRejoining(callExtId);
807
807
  if (fallback.current) clearTimeout(fallback.current);
808
808
  fallback.current = setTimeout(() => void room.disconnect(), DEMOTE_REJOIN_FALLBACK_MS);
809
809
  return;
@@ -830,12 +830,12 @@ function useWebinar({
830
830
  [callExtId, linkVersion]
831
831
  );
832
832
  const stepDown = react.useCallback(async () => {
833
- chunkEOKCXEYN_cjs.markRejoining(callExtId);
833
+ chunkX4HGUBWU_cjs.markRejoining(callExtId);
834
834
  setStageCheck(false);
835
835
  try {
836
836
  await webinarApi().demote(callExtId, myIdentity, linkVersion);
837
837
  } catch (err) {
838
- chunkEOKCXEYN_cjs.markRejoining(callExtId, false);
838
+ chunkX4HGUBWU_cjs.markRejoining(callExtId, false);
839
839
  failed("leave the stage")(err);
840
840
  }
841
841
  }, [callExtId, myIdentity, linkVersion]);
@@ -895,7 +895,7 @@ function useWebinar({
895
895
  if (!callKey) throw new Error("This call has no key.");
896
896
  await webinarApi().questions.ask(
897
897
  callExtId,
898
- await chunkEOKCXEYN_cjs.sealQuestionText(text, callKey),
898
+ await chunkX4HGUBWU_cjs.sealQuestionText(text, callKey),
899
899
  linkVersion
900
900
  );
901
901
  }, "ask the question"),
@@ -909,7 +909,7 @@ function useWebinar({
909
909
  await webinarApi().questions.update(
910
910
  callExtId,
911
911
  id,
912
- { answer: await chunkEOKCXEYN_cjs.sealQuestionText(text, callKey) },
912
+ { answer: await chunkX4HGUBWU_cjs.sealQuestionText(text, callKey) },
913
913
  linkVersion
914
914
  );
915
915
  }, "answer the question"),
@@ -939,15 +939,15 @@ function useWebinar({
939
939
  function useHostMuteNotice(callExtId) {
940
940
  const room = componentsReact.useRoomContext();
941
941
  react.useEffect(() => {
942
- const notifyMuted = chunkEOKCXEYN_cjs.createMutedNotifier((text) => chunkRLSBRZ6Z_cjs.notify.info(text));
942
+ const notifyMuted = chunkX4HGUBWU_cjs.createMutedNotifier((text) => chunkRLSBRZ6Z_cjs.notify.info(text));
943
943
  const onData = (payload, participant, _kind, topic, _encryption, participantIdentity) => {
944
- if (topic !== chunkEOKCXEYN_cjs.CALL_MODERATION_TOPIC) return;
944
+ if (topic !== chunkX4HGUBWU_cjs.CALL_MODERATION_TOPIC) return;
945
945
  if (participant || participantIdentity) return;
946
- const notice = chunkEOKCXEYN_cjs.mutedNoticeFromPacket(payload);
946
+ const notice = chunkX4HGUBWU_cjs.mutedNoticeFromPacket(payload);
947
947
  if (notice) notifyMuted(notice);
948
948
  };
949
949
  room.on(livekitClient.RoomEvent.DataReceived, onData);
950
- const offWs = chunkEOKCXEYN_cjs.onCallEvent("call.muted", (message) => {
950
+ const offWs = chunkX4HGUBWU_cjs.onCallEvent("call.muted", (message) => {
951
951
  if (message.callExtId === callExtId) notifyMuted(message);
952
952
  });
953
953
  return () => {
@@ -1,4 +1,4 @@
1
- import { toProfileView, useCallTranscriptionAvailable, onCallEvent, transcriptPersistRole, persistTranscriptSegment, newSegmentId, encodeCaptionPacket, CALL_CAPTION_TOPIC, ensureMeetingChat, trackUploaderFor, conversationKeyForCall, loadMeetingHistory, CALL_CHAT_MAX_CHARS, newPacketId, encodeCallChatPacket, CALL_CHAT_TOPIC, persistMeetingMessage, mergeChatLines, openQuestion, markRejoining, sealQuestionText, createMutedNotifier, CALL_RECORDING_TOPIC, CALL_TRANSCRIPTION_TOPIC, decodeCaptionPacket, CALL_MODERATION_TOPIC, mutedNoticeFromPacket, decodeCallChatPacket, CALL_WEBINAR_TOPIC, parseWebinarPacket } from '../../chunk-AORG37H6.js';
1
+ import { toProfileView, useCallTranscriptionAvailable, onCallEvent, transcriptPersistRole, persistTranscriptSegment, newSegmentId, encodeCaptionPacket, CALL_CAPTION_TOPIC, ensureMeetingChat, trackUploaderFor, conversationKeyForCall, loadMeetingHistory, CALL_CHAT_MAX_CHARS, newPacketId, encodeCallChatPacket, CALL_CHAT_TOPIC, persistMeetingMessage, mergeChatLines, openQuestion, markRejoining, sealQuestionText, createMutedNotifier, CALL_RECORDING_TOPIC, CALL_TRANSCRIPTION_TOPIC, decodeCaptionPacket, CALL_MODERATION_TOPIC, mutedNoticeFromPacket, decodeCallChatPacket, CALL_WEBINAR_TOPIC, parseWebinarPacket } from '../../chunk-RIPYDSRJ.js';
2
2
  import '../../chunk-N3SFVZ7H.js';
3
3
  import { useArbi, useArbiHost } from '../../chunk-7PMNITKA.js';
4
4
  import { notify } from '../../chunk-BF5KW5ZI.js';
@@ -5,7 +5,7 @@ import { getUserDisplayName, DEFAULT_SEGMENTER_CONFIG, INITIAL_SEGMENTER_STATE,
5
5
  import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query';
6
6
  import { useSyncExternalStore, useEffect, useState, useCallback, useMemo } from 'react';
7
7
  import { chats, calls, ArbiApiError, CallApiError, documents, continuityPrompt } from '@arbidocs/sdk/browser';
8
- import { generateChatKey, sealCallKey, sealChatKey, decryptCallTitle, encryptWithChatKey, decryptWithChatKey, initSodium, bytesToBase64, encryptFile, decryptFile, base64ToBytes, buildCallLink, encryptCallTitle } from '@arbidocs/client';
8
+ import { createOfflineSealer, generateChatKey, sealCallKey, sealChatKey, decryptCallTitle, encryptWithChatKey, decryptWithChatKey, initSodium, bytesToBase64, encryptFile, decryptFile, base64ToBytes, buildCallLink, encryptCallTitle } from '@arbidocs/client';
9
9
 
10
10
  function useUserSettings(options) {
11
11
  const arbi = useArbi();
@@ -94,7 +94,7 @@ function createLocalFeatureStore({
94
94
  storage = defaultStorage(),
95
95
  migrate
96
96
  }) {
97
- const listeners3 = /* @__PURE__ */ new Set();
97
+ const listeners4 = /* @__PURE__ */ new Set();
98
98
  const read = () => {
99
99
  try {
100
100
  return storage?.getItem(key) ?? null;
@@ -108,7 +108,7 @@ function createLocalFeatureStore({
108
108
  } catch {
109
109
  }
110
110
  };
111
- const notify2 = () => listeners3.forEach((listener) => listener());
111
+ const notify2 = () => listeners4.forEach((listener) => listener());
112
112
  const saved = parseValues(read());
113
113
  let values = saved ?? {};
114
114
  let hydrated = saved !== null || !migrate;
@@ -130,8 +130,8 @@ function createLocalFeatureStore({
130
130
  notify2();
131
131
  },
132
132
  subscribe(listener) {
133
- listeners3.add(listener);
134
- return () => listeners3.delete(listener);
133
+ listeners4.add(listener);
134
+ return () => listeners4.delete(listener);
135
135
  },
136
136
  isHydrated: () => hydrated,
137
137
  hydrated: () => ready
@@ -666,6 +666,253 @@ function applyCallEvent(queryClient, event, options = {}) {
666
666
  return false;
667
667
  }
668
668
  }
669
+ var OfflineMailLockedError = class extends Error {
670
+ constructor() {
671
+ super("Sign in to open the mail saved on this device");
672
+ this.name = "OfflineMailLockedError";
673
+ }
674
+ };
675
+ var sealer = createOfflineSealer("arbi-offline-mail-v1", {
676
+ locked: () => new OfflineMailLockedError()
677
+ });
678
+ var forgetOfflineMailKeys = sealer.forget;
679
+ var sealBytes = sealer.sealBytes;
680
+ var openBytes = sealer.openBytes;
681
+ var sealJson = sealer.sealJson;
682
+ var openJson = sealer.openJson;
683
+ var blind = sealer.blind;
684
+
685
+ // src/offline-store/store.ts
686
+ var DB_VERSION = 1;
687
+ var RECORDS = "records";
688
+ var META = "meta";
689
+ function promisify(request) {
690
+ return new Promise((resolve, reject) => {
691
+ request.onsuccess = () => resolve(request.result);
692
+ request.onerror = () => reject(request.error);
693
+ });
694
+ }
695
+ function createRecordStore(dbName) {
696
+ let dbPromise = null;
697
+ function openDB() {
698
+ if (dbPromise) return dbPromise;
699
+ dbPromise = new Promise((resolve, reject) => {
700
+ const request = indexedDB.open(dbName, DB_VERSION);
701
+ request.onerror = () => {
702
+ dbPromise = null;
703
+ reject(request.error);
704
+ };
705
+ request.onsuccess = () => {
706
+ const db = request.result;
707
+ db.onversionchange = () => {
708
+ db.close();
709
+ dbPromise = null;
710
+ };
711
+ resolve(db);
712
+ };
713
+ request.onupgradeneeded = () => {
714
+ const db = request.result;
715
+ if (!db.objectStoreNames.contains(RECORDS)) {
716
+ const records = db.createObjectStore(RECORDS, { keyPath: "key" });
717
+ records.createIndex("bucket", "bucket");
718
+ records.createIndex("kind", "kind");
719
+ }
720
+ if (!db.objectStoreNames.contains(META)) db.createObjectStore(META, { keyPath: "key" });
721
+ };
722
+ });
723
+ return dbPromise;
724
+ }
725
+ async function run(store2, mode, fn) {
726
+ const db = await openDB();
727
+ return promisify(fn(db.transaction(store2, mode).objectStore(store2)));
728
+ }
729
+ async function scan(pick) {
730
+ const db = await openDB();
731
+ const store2 = db.transaction(RECORDS, "readonly").objectStore(RECORDS);
732
+ return new Promise((resolve, reject) => {
733
+ const out = [];
734
+ const cursor = store2.openCursor();
735
+ cursor.onerror = () => reject(cursor.error);
736
+ cursor.onsuccess = () => {
737
+ const c = cursor.result;
738
+ if (!c) return resolve(out);
739
+ out.push(pick(c.value));
740
+ c.continue();
741
+ };
742
+ });
743
+ }
744
+ const self = {
745
+ dbName,
746
+ async putRecord(record) {
747
+ await run(RECORDS, "readwrite", (s) => s.put(record));
748
+ },
749
+ async getRecord(key) {
750
+ return await run(RECORDS, "readonly", (s) => s.get(key));
751
+ },
752
+ async hasRecord(key) {
753
+ return await run(RECORDS, "readonly", (s) => s.count(key)) > 0;
754
+ },
755
+ async deleteRecords(keys) {
756
+ if (keys.length === 0) return;
757
+ const db = await openDB();
758
+ const tx = db.transaction(RECORDS, "readwrite");
759
+ const store2 = tx.objectStore(RECORDS);
760
+ for (const key of keys) store2.delete(key);
761
+ await new Promise((resolve, reject) => {
762
+ tx.oncomplete = () => resolve();
763
+ tx.onerror = () => reject(tx.error);
764
+ tx.onabort = () => reject(tx.error);
765
+ });
766
+ },
767
+ async recordsInBucket(bucket) {
768
+ return await run(
769
+ RECORDS,
770
+ "readonly",
771
+ (s) => s.index("bucket").getAll(bucket)
772
+ );
773
+ },
774
+ async recordsOfKind(kind) {
775
+ return await run(
776
+ RECORDS,
777
+ "readonly",
778
+ (s) => s.index("kind").getAll(kind)
779
+ );
780
+ },
781
+ allRecordSizes() {
782
+ return scan(({ key, kind, size }) => ({ key, kind, size }));
783
+ },
784
+ allRecordAccess() {
785
+ return scan(({ key, kind, lastAccess }) => ({ key, kind, lastAccess }));
786
+ },
787
+ async touchRecord(key) {
788
+ const record = await self.getRecord(key);
789
+ if (record) await self.putRecord({ ...record, lastAccess: Date.now() });
790
+ },
791
+ async getMeta(key) {
792
+ const row = await run(META, "readonly", (s) => s.get(key));
793
+ return row?.value;
794
+ },
795
+ async setMeta(key, value) {
796
+ await run(META, "readwrite", (s) => s.put({ key, value }));
797
+ },
798
+ async deleteDatabase() {
799
+ const db = await dbPromise?.catch(() => null);
800
+ db?.close();
801
+ dbPromise = null;
802
+ await new Promise((resolve, reject) => {
803
+ const request = indexedDB.deleteDatabase(dbName);
804
+ request.onsuccess = () => resolve();
805
+ request.onerror = () => reject(request.error);
806
+ request.onblocked = () => {
807
+ };
808
+ });
809
+ }
810
+ };
811
+ return self;
812
+ }
813
+
814
+ // src/mail/offline/store.ts
815
+ var OFFLINE_MAIL_DB = "arbi-offline-mail";
816
+ var store = createRecordStore(OFFLINE_MAIL_DB);
817
+ var putRecord = store.putRecord;
818
+ var getRecord = store.getRecord;
819
+ var hasRecord = store.hasRecord;
820
+ var deleteRecords = store.deleteRecords;
821
+ var recordsInBucket = store.recordsInBucket;
822
+ var recordsOfKind = store.recordsOfKind;
823
+ var allRecordSizes = store.allRecordSizes;
824
+ var touchRecord = store.touchRecord;
825
+ var getMeta = store.getMeta;
826
+ var setMeta = store.setMeta;
827
+ var deleteOfflineMailDatabase = store.deleteDatabase;
828
+
829
+ // src/mail/offline/settings.ts
830
+ var DEFAULT_OFFLINE_MAIL_SETTINGS = {
831
+ enabled: true,
832
+ scope: "7d",
833
+ attachmentCapMb: 25,
834
+ quotaMb: 1024
835
+ };
836
+ var SCOPE_DAYS = {
837
+ "1d": 1,
838
+ "7d": 7,
839
+ "30d": 30,
840
+ "90d": 90
841
+ };
842
+ var DAY_MS = 24 * 60 * 60 * 1e3;
843
+ function offlineWindowStart(settings, now = Date.now()) {
844
+ return now - SCOPE_DAYS[settings.scope] * DAY_MS;
845
+ }
846
+ function inOfflineWindow(settings, receivedAt, now = Date.now()) {
847
+ const at = receivedAt ? Date.parse(receivedAt) || 0 : 0;
848
+ return settings.enabled && (!at || at >= offlineWindowStart(settings, now));
849
+ }
850
+ function normalize(saved) {
851
+ const merged = { ...DEFAULT_OFFLINE_MAIL_SETTINGS, ...saved };
852
+ const scope = merged.scope;
853
+ if (scope in SCOPE_DAYS) return merged;
854
+ const longer = scope === "365d" || scope === "all";
855
+ return { ...merged, scope: longer ? "90d" : DEFAULT_OFFLINE_MAIL_SETTINGS.scope };
856
+ }
857
+ var STORAGE_PREFIX = "arbi-offline-mail-settings:";
858
+ var SIGNED_OUT = { ...DEFAULT_OFFLINE_MAIL_SETTINGS, enabled: false };
859
+ function getOfflineMailSettings(userExtId) {
860
+ if (!userExtId) return SIGNED_OUT;
861
+ const key = STORAGE_PREFIX + userExtId;
862
+ try {
863
+ const raw = localStorage.getItem(key);
864
+ return raw ? normalize(JSON.parse(raw)) : DEFAULT_OFFLINE_MAIL_SETTINGS;
865
+ } catch {
866
+ return DEFAULT_OFFLINE_MAIL_SETTINGS;
867
+ }
868
+ }
869
+ function setOfflineMailSettings(userExtId, patch) {
870
+ const next = { ...getOfflineMailSettings(userExtId), ...patch };
871
+ if (userExtId) {
872
+ try {
873
+ localStorage.setItem(STORAGE_PREFIX + userExtId, JSON.stringify(next));
874
+ } catch {
875
+ }
876
+ }
877
+ notifyOfflineMailChange("settings");
878
+ return next;
879
+ }
880
+ var listeners3 = /* @__PURE__ */ new Set();
881
+ var CHANNEL = "arbi-offline-mail";
882
+ var channel = null;
883
+ function getChannel() {
884
+ if (channel || typeof BroadcastChannel === "undefined") return channel;
885
+ channel = new BroadcastChannel(CHANNEL);
886
+ channel.onmessage = (event) => {
887
+ for (const listener of listeners3) listener(event.data);
888
+ };
889
+ return channel;
890
+ }
891
+ function notifyOfflineMailChange(change) {
892
+ for (const listener of listeners3) listener(change);
893
+ try {
894
+ getChannel()?.postMessage(change);
895
+ } catch {
896
+ }
897
+ }
898
+ function onOfflineMailChange(listener) {
899
+ getChannel();
900
+ listeners3.add(listener);
901
+ return () => listeners3.delete(listener);
902
+ }
903
+
904
+ // src/mail/offline/wipe.ts
905
+ var OWNER_KEY = "owner";
906
+ async function wipeOfflineMail() {
907
+ forgetOfflineMailKeys();
908
+ await deleteOfflineMailDatabase();
909
+ notifyOfflineMailChange("wiped");
910
+ }
911
+ async function claimOfflineMail(userExtId) {
912
+ const owner = await getMeta(OWNER_KEY);
913
+ if (owner && owner !== userExtId) await wipeOfflineMail();
914
+ await setMeta(OWNER_KEY, userExtId);
915
+ }
669
916
  function toProfileView(profile) {
670
917
  return {
671
918
  ...profile,
@@ -801,6 +1048,8 @@ function arbiAccountClient(arbi, overrides = {}) {
801
1048
  } finally {
802
1049
  clearSealedDrafts();
803
1050
  clearChatArtifacts();
1051
+ await wipeOfflineMail().catch(() => {
1052
+ });
804
1053
  }
805
1054
  },
806
1055
  requestDeletion: () => arbi.account.requestDeletion(),
@@ -2473,6 +2722,6 @@ function useOpenCallMeetingChat() {
2473
2722
  );
2474
2723
  }
2475
2724
 
2476
- export { AUDIENCE_CHOICES, CALL_CAPTION_TOPIC, CALL_CHAT_MAX_CHARS, CALL_CHAT_TOPIC, CALL_MODERATION_TOPIC, CALL_RECORDING_TOPIC, CALL_TRANSCRIPTION_TOPIC, CALL_WEBINAR_TOPIC, CAPTION_MAX_CHARS, COMPOSITE_AUDIO_BITS_PER_SECOND, COMPOSITE_VIDEO_BITS_PER_SECOND, DEFAULT_JOIN_CHOICES, EMPTY_CALL_SESSION, EXPORT_SAMPLE_RATE, HISTORY_LIMIT, LIVE_SEGMENTER, MAX_AVATAR_FILE_BYTES, MUTED_DEDUPE_MS, PLAYBACK_LOOKAHEAD_MS, READ_ONLY_MEETING_TEXT, RECORDING_CHUNK_MS, RECORDING_SEGMENTER, SEGMENT_FRAME_MS, SEGMENT_ID_PATTERN, SEGMENT_PRE_ROLL_MS, SHORT_PIECE_MERGE_GAP_MS, SPEAKER_AUDIO_BITS_PER_SECOND, TrackUploader, UPLOAD_ATTEMPTS, UPLOAD_BACKOFF_MS, acceptRing, addConversationToList, appendToThread, applyAgentDmStatus, applyCallEvent, applyChatEvent, applyChatMessage, applyChatRead, applyChatUpdated, arbiAccountClient, buildMeetingChatBody, cachedChat, cachedChats, callLink, callMemberKeys, callSession, chatByExtId, chatHasLiveCall, chatLiveCallId, chatTranscriptRef, clearMeetingChatCache, clearTrackUploaders, clockTime, compositeFiles, compositeTrack, conversationKeyForCall, createLocalFeatureStore, createMutedNotifier, createTrackCutter, cutAtPauses, declineRing, decodeCallChatPacket, decodeCaptionPacket, decodeMono16k, decryptPart, deleteChatMessage, dropFromThread, encodeCallChatPacket, encodeCaptionPacket, encodeWav, encryptPart, ensureMeetingChat, entitlementRefusal, extensionFor, fetchPart, fileSafe, findConversation, floatToWav, formatTranscriptMarkdown, getOpenChat, hasRecordingTranscript, invalidateCall, invalidateConversationDetails, invalidateConversationLists, isConversationListKey, isConversationShared, isRecordingOver, isRecordingSegmentId, isRetryable, isWorkspaceGuestOf, joinFromPreJoin, knownMeetingChat, loadMeetingHistory, markRejoining, meetingChatRefFrom, meetingRefForCall, mergeChatLines, mutedNoticeFromEvent, mutedNoticeFromPacket, mutedNoticeText, myChatId, newPacketId, newSegmentId, newTrackKey, nextSeqAfter, onCallEvent, openCall, openQuestion, openRecording, parseWebinarPacket, partKey, partOffsetMs, partsDue, patchConversation, patchConversationList, persistMeetingMessage, persistTranscriptSegment, playheadAt, postRecordingDocsToThread, postTranscriptToThread, readLocalFeature, recordingFiles, recordingWindowsOf, rejoinAsAudience, removeChat, removeConversationFromLists, resetCallSession, resolveMeetingChat, saveRecordingFilesToThread, schedulePart, sealQuestionText, serverCallsExperimental, setOpenChat, sortTranscriptMessages, sortedParts, speakerTracks, speakerWav, splitIntoFiles, standalone, startChatCall, startContactsCall, startDmCall, startThreadCall, startsMediaFile, storeTranscriptSegment, stripSpeakerPrefix, supersedeLiveTranscript, supersededTranscriptLines, threadHasLiveCall, threadLiveCallId, threadTranscriptLines, threadTranscriptRef, threadTranscriptText, timelineDurationMs, timelineOf, toProfileView, trackUploaderFor, transcribeRecording, transcriptPersistRole, transcriptSegmentOf, unwrapTrackKey, upsertChat, useAccountClient, useAvatarCrop, useCall, useCallInviteLinks, useCallRecording, useCallRecordings, useCallSession, useCallTranscriptionAvailable, useCallsConfig, useCallsEnabled, useCanHostCalls, useEntitlements, useFeature, useFeatureSwitch, useInvalidateEntitlements, useIsEntitled, useMeetingReadOnly, useNameEditor, useOpenCallMeetingChat, useProfile, useRecordingActions, useRecordingWindows, useRevokeCallInviteLink, useSaveToThread, useServerCallsEnabled, useServerCallsExperimental, useUpdateProfile, useUpdateUserSettings, useUserSettings, wrapTrackKey, writeSamples };
2477
- //# sourceMappingURL=chunk-AORG37H6.js.map
2478
- //# sourceMappingURL=chunk-AORG37H6.js.map
2725
+ export { AUDIENCE_CHOICES, CALL_CAPTION_TOPIC, CALL_CHAT_MAX_CHARS, CALL_CHAT_TOPIC, CALL_MODERATION_TOPIC, CALL_RECORDING_TOPIC, CALL_TRANSCRIPTION_TOPIC, CALL_WEBINAR_TOPIC, CAPTION_MAX_CHARS, COMPOSITE_AUDIO_BITS_PER_SECOND, COMPOSITE_VIDEO_BITS_PER_SECOND, DEFAULT_JOIN_CHOICES, EMPTY_CALL_SESSION, EXPORT_SAMPLE_RATE, HISTORY_LIMIT, LIVE_SEGMENTER, MAX_AVATAR_FILE_BYTES, MUTED_DEDUPE_MS, OFFLINE_MAIL_DB, PLAYBACK_LOOKAHEAD_MS, READ_ONLY_MEETING_TEXT, RECORDING_CHUNK_MS, RECORDING_SEGMENTER, SCOPE_DAYS, SEGMENT_FRAME_MS, SEGMENT_ID_PATTERN, SEGMENT_PRE_ROLL_MS, SHORT_PIECE_MERGE_GAP_MS, SPEAKER_AUDIO_BITS_PER_SECOND, TrackUploader, UPLOAD_ATTEMPTS, UPLOAD_BACKOFF_MS, acceptRing, addConversationToList, allRecordSizes, appendToThread, applyAgentDmStatus, applyCallEvent, applyChatEvent, applyChatMessage, applyChatRead, applyChatUpdated, arbiAccountClient, blind, buildMeetingChatBody, cachedChat, cachedChats, callLink, callMemberKeys, callSession, chatByExtId, chatHasLiveCall, chatLiveCallId, chatTranscriptRef, claimOfflineMail, clearMeetingChatCache, clearTrackUploaders, clockTime, compositeFiles, compositeTrack, conversationKeyForCall, createLocalFeatureStore, createMutedNotifier, createRecordStore, createTrackCutter, cutAtPauses, declineRing, decodeCallChatPacket, decodeCaptionPacket, decodeMono16k, decryptPart, deleteChatMessage, deleteRecords, dropFromThread, encodeCallChatPacket, encodeCaptionPacket, encodeWav, encryptPart, ensureMeetingChat, entitlementRefusal, extensionFor, fetchPart, fileSafe, findConversation, floatToWav, formatTranscriptMarkdown, getMeta, getOfflineMailSettings, getOpenChat, getRecord, hasRecord, hasRecordingTranscript, inOfflineWindow, invalidateCall, invalidateConversationDetails, invalidateConversationLists, isConversationListKey, isConversationShared, isRecordingOver, isRecordingSegmentId, isRetryable, isWorkspaceGuestOf, joinFromPreJoin, knownMeetingChat, loadMeetingHistory, markRejoining, meetingChatRefFrom, meetingRefForCall, mergeChatLines, mutedNoticeFromEvent, mutedNoticeFromPacket, mutedNoticeText, myChatId, newPacketId, newSegmentId, newTrackKey, nextSeqAfter, notifyOfflineMailChange, offlineWindowStart, onCallEvent, onOfflineMailChange, openBytes, openCall, openJson, openQuestion, openRecording, parseWebinarPacket, partKey, partOffsetMs, partsDue, patchConversation, patchConversationList, persistMeetingMessage, persistTranscriptSegment, playheadAt, postRecordingDocsToThread, postTranscriptToThread, putRecord, readLocalFeature, recordingFiles, recordingWindowsOf, recordsInBucket, recordsOfKind, rejoinAsAudience, removeChat, removeConversationFromLists, resetCallSession, resolveMeetingChat, saveRecordingFilesToThread, schedulePart, sealBytes, sealJson, sealQuestionText, serverCallsExperimental, setMeta, setOfflineMailSettings, setOpenChat, sortTranscriptMessages, sortedParts, speakerTracks, speakerWav, splitIntoFiles, standalone, startChatCall, startContactsCall, startDmCall, startThreadCall, startsMediaFile, storeTranscriptSegment, stripSpeakerPrefix, supersedeLiveTranscript, supersededTranscriptLines, threadHasLiveCall, threadLiveCallId, threadTranscriptLines, threadTranscriptRef, threadTranscriptText, timelineDurationMs, timelineOf, toProfileView, touchRecord, trackUploaderFor, transcribeRecording, transcriptPersistRole, transcriptSegmentOf, unwrapTrackKey, upsertChat, useAccountClient, useAvatarCrop, useCall, useCallInviteLinks, useCallRecording, useCallRecordings, useCallSession, useCallTranscriptionAvailable, useCallsConfig, useCallsEnabled, useCanHostCalls, useEntitlements, useFeature, useFeatureSwitch, useInvalidateEntitlements, useIsEntitled, useMeetingReadOnly, useNameEditor, useOpenCallMeetingChat, useProfile, useRecordingActions, useRecordingWindows, useRevokeCallInviteLink, useSaveToThread, useServerCallsEnabled, useServerCallsExperimental, useUpdateProfile, useUpdateUserSettings, useUserSettings, wipeOfflineMail, wrapTrackKey, writeSamples };
2726
+ //# sourceMappingURL=chunk-RIPYDSRJ.js.map
2727
+ //# sourceMappingURL=chunk-RIPYDSRJ.js.map