@arbidocs/react 0.5.84 → 0.5.86
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 +31 -0
- package/dist/index.cjs +1736 -148
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +272 -1
- package/dist/index.d.ts +272 -1
- package/dist/index.js +1718 -152
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { AudioViewer, ImageViewer, VideoViewer } from './chunk-HDCRPPBW.js';
|
|
2
|
-
import { useUserSettings, invalidateConversationLists, patchConversation, invalidateConversationDetails, removeConversationFromLists, addConversationToList, findConversation, isConversationShared, patchConversationList, useFeature, useAccountClient, useUpdateUserSettings, myChatId, upsertChat, appendToThread, dropFromThread, deleteChatMessage, removeChat, getOpenChat, cachedChat, applyChatEvent, applyCallEvent } from './chunk-WQY3RQ6G.js';
|
|
2
|
+
import { useUserSettings, invalidateConversationLists, patchConversation, invalidateConversationDetails, removeConversationFromLists, addConversationToList, findConversation, isConversationShared, patchConversationList, useFeature, useAccountClient, useUpdateUserSettings, useIsEntitled, myChatId, upsertChat, appendToThread, dropFromThread, deleteChatMessage, removeChat, getOpenChat, cachedChat, applyChatEvent, applyCallEvent } from './chunk-WQY3RQ6G.js';
|
|
3
3
|
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, 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 } from './chunk-WQY3RQ6G.js';
|
|
4
4
|
import { replayableTagMutations, inWorkspace, primeOnClient, patchDocuments, setCachedDocument, adjustTagCounts, patchCachedDocument, FINISHED_DOCUMENT_STATUSES, refreshDocuments, getCachedDocuments, useArbiWsStore, IN_FLIGHT_DOCUMENT_STATUSES, useCapturingWorkspace, QUEUED_WRITE_RETRY, isWorkspaceUnavailable, primeOnArbi, forgetDocuments, useDocument, useSimilarDocuments, SIMILARITY_THRESHOLD, QUEUED_WRITE_BLOCKED_MESSAGE, isBlockedOnWorkspace, useDocuments, finishEntityExtraction } from './chunk-VGAA7AFS.js';
|
|
5
5
|
export { DISMISSED_TASK_TTL_MS, DOCUMENTS_FIRST_PAGE_MEASURE, DOCUMENTS_FIRST_PAGE_SIZE, DOCUMENTS_PAGE_SIZE, FINISHED_DOCUMENT_STATUSES, IN_FLIGHT_DOCUMENT_STATUSES, SIMILARITY_THRESHOLD, adjustTagCounts, dropTagFromCachedDocuments, finishEntityExtraction, forgetDocuments, getCachedDocuments, invalidateTagData, invalidateTagTables, isDocumentsLoading, isRowNewer, isStaleTaskUpdate, patchCachedDocument, patchDocuments, patchWorkspaceTags, refreshDocuments, resolveDocumentStatus, setCachedDocument, settledByRows, startEntityExtraction, stopDocumentsLoading, subscribeDocumentRows, upsertDocuments, useArbiWsStore, useCreateTag, useDeleteTag, useDocument, useDocuments, useDocumentsById, useDocumentsLoading, useEntityExtractionDocIds, useExtractEntities, useInvalidateTagData, useIsCreatingTag, useRefreshDocuments, useSetTagValues, useSimilarDocuments, useTagKindSchema, useUpdateTag, useUpdateTagKindSchema } from './chunk-VGAA7AFS.js';
|
|
6
6
|
import { useArbiSessionState, useArbiSession, useWorkspaces, useConversations, useConfig, useArbiSessionStore, useAvailableModels, useParsedDocument, useTags, selectDraft, useConversationThreads, createArbiSessionStore, useTransientView, useProjects, useAgents, useAssistantQuery, useActivity, queuedMessages, MIN_QUEUED_MESSAGE_VISIBILITY_MS } from './chunk-XPLD4LFC.js';
|
|
7
7
|
export { DEFAULT_DOCUMENT_VIEWER_STATE, DOCUMENT_VIEWER_PANEL, EMPTY_CHAT_STATE, EMPTY_DOCUMENT_STATE, EMPTY_SESSION_STATE, MAX_THREAD_TABS, MIN_QUEUED_MESSAGE_VISIBILITY_MS, PROJECT_CREATION_DENIED_REASON, WORKSPACE_LAST_OPENED_KEY, WORKSPACE_STORAGE_KEY, agentControl, browserWorkspaceStorage, clearSealedDrafts, closePanelIn, composeArbiSessionStore, conversationKey, createArbiChatStore, createArbiDocumentSessionStore, createArbiLayoutStore, createArbiSessionStore, createArbiWorkspaceStore, createDebouncedWriter, createSealedDraftStorage, createStoragePersist, createUiStateStorage, defaultLayoutState, dmTabId, flushOnPageExit, isPanelOpen, isUsageMetered, layoutActionsOf, nextViewingDocument, openPanelIn, panelPlace, parseSavedChat, parseSavedDocuments, parseSavedLayout, queuedMessages, readWorkspaceLastOpened, restoreLayout, restoreSavedDocuments, savedChatOf, savedDocumentsOf, savedLayoutOf, selectDraft, selectIsGenerating, selectViewingDocumentId, threadTabId, useActivity, useAgents, useArbiSession, useArbiSessionState, useArbiSessionStore, useAssistantQuery, useAvailableModels, useBoxRole, useBuiltinCommands, useCanCreateProjects, useConfig, useConfigs, useConversationThreads, useConversations, useCurrentUser, useDeleteAgents, useDocumentVersionList, useFirmPool, useHealth, useMessageDetails, useParsedDocument, useProjectInvoices, useProjectUsage, useProjects, useProjectsUsage, useSessionPageTab, useSessionView, useSignedInSession, useTags, useThumbnails, useTopUpFirmPool, useTransientView, useUserUsageToday, useWorkspaceActivity, useWorkspaces } from './chunk-XPLD4LFC.js';
|
|
8
8
|
import { fetchDocumentFile, downloadBlob, fetchMarkupPdf, fetchDocumentPdf } from './chunk-47YVW3J4.js';
|
|
9
|
-
import { arbiMutationKeys, arbiQueryKeys, mailQueryKeys, connectorQueryKeys, calendarQueryKeys } from './chunk-TK7ERJPE.js';
|
|
9
|
+
import { arbiMutationKeys, arbiQueryKeys, mailQueryKeys, isUnderQueryKey, connectorQueryKeys, calendarQueryKeys } from './chunk-TK7ERJPE.js';
|
|
10
10
|
export { arbiMutationKeys, arbiQueryKeys, calendarQueryKeys, callQueryKeys, connectorQueryKeys, isUnderQueryKey, mailQueryKeys } from './chunk-TK7ERJPE.js';
|
|
11
11
|
import { useArbi, useArbiHost } from './chunk-VGO77X5U.js';
|
|
12
12
|
export { useArbi, useArbiHost, useDeploymentDomain } from './chunk-VGO77X5U.js';
|
|
@@ -41,7 +41,7 @@ export { ACCOUNT_VENDOR, Arbi, ArbiError, DEFAULT_AI_USD_PER_GBP, DRIVE_SYNC_VEN
|
|
|
41
41
|
import { Link, Trash2, CheckCheck, Check, MoreHorizontal, SquarePen, Pencil, Download, X, Mic, Pause, Play, Square, Loader2, RotateCcw, Sparkles, Camera, Circle, AlertTriangle, ShieldCheck, Copy, FileText, GitCompareArrows, Search, CornerDownLeft, Send, Tags, Info, Activity, HardDrive, Gauge, Gift, PartyPopper, UserPlus, UserCheck, Reply, PenLine, AtSign, MessageCircleMore, FileX2, FileCheck2, FolderPlus, SquareArrowOutUpRight, Paperclip, ArrowRight, Image as Image$1, AppWindow } from 'lucide-react';
|
|
42
42
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
43
43
|
import { create } from 'zustand';
|
|
44
|
-
import { getUserEncryptionKeypair, sealedBoxDecrypt, bytesToBase64, isMessageType, decryptMessage, SETTINGS_WRITE_BREAKER_CODE, passwordChangeErrorMessage, createRecoveryFile, isAbortError, base64ToBytes, sealedBoxEncrypt
|
|
44
|
+
import { createOfflineSealer, getUserEncryptionKeypair, sealedBoxDecrypt, bytesToBase64, isMessageType, decryptMessage, SETTINGS_WRITE_BREAKER_CODE, passwordChangeErrorMessage, createRecoveryFile, logger, isAbortError, base64ToBytes, sealedBoxEncrypt } from '@arbidocs/client';
|
|
45
45
|
export { isMessageType } from '@arbidocs/client';
|
|
46
46
|
import { get, del, set } from 'idb-keyval';
|
|
47
47
|
import { diffWords } from 'diff';
|
|
@@ -185,8 +185,8 @@ function useStartTaskQuery() {
|
|
|
185
185
|
};
|
|
186
186
|
const lookUpThread = async (messageId, attempts, known) => {
|
|
187
187
|
let conversationId;
|
|
188
|
-
for (let
|
|
189
|
-
if (
|
|
188
|
+
for (let attempt2 = 0; attempt2 < attempts && !conversationId; attempt2 += 1) {
|
|
189
|
+
if (attempt2 > 0) await new Promise((r) => setTimeout(r, THREAD_LOOKUP_DELAY_MS));
|
|
190
190
|
try {
|
|
191
191
|
conversationId = await threadOf(messageId, known);
|
|
192
192
|
} catch (error) {
|
|
@@ -1806,9 +1806,9 @@ function useWorkspaceProcessing(workspaceId) {
|
|
|
1806
1806
|
}, []);
|
|
1807
1807
|
useEffect(() => {
|
|
1808
1808
|
if (!workspaceId) return;
|
|
1809
|
-
const
|
|
1809
|
+
const store3 = useArbiWsStore.getState();
|
|
1810
1810
|
const documents2 = getCachedDocuments(queryClient, workspaceId) ?? [];
|
|
1811
|
-
|
|
1811
|
+
store3.settleTasks(documents2);
|
|
1812
1812
|
const listed = new Set(documents2.map((d) => d.external_id));
|
|
1813
1813
|
const now = Date.now();
|
|
1814
1814
|
for (const task of Object.values(useArbiWsStore.getState().tasks)) {
|
|
@@ -1818,7 +1818,7 @@ function useWorkspaceProcessing(workspaceId) {
|
|
|
1818
1818
|
const age = since == null ? null : now - since;
|
|
1819
1819
|
const stale = age != null && age > STALE_TASK_MS;
|
|
1820
1820
|
const orphan = documents2.length > 0 && !listed.has(task.doc_ext_id) && age != null && age > ORPHAN_TASK_MS;
|
|
1821
|
-
if (stale || orphan)
|
|
1821
|
+
if (stale || orphan) store3.clearTask(task.doc_ext_id);
|
|
1822
1822
|
}
|
|
1823
1823
|
}, [workspaceId, queryClient]);
|
|
1824
1824
|
useEffect(() => {
|
|
@@ -1863,25 +1863,25 @@ function useWorkspaceProcessing(workspaceId) {
|
|
|
1863
1863
|
}, [workspaceId, snapshot]);
|
|
1864
1864
|
}
|
|
1865
1865
|
function applyOptimisticDocumentEdit(qc, updates) {
|
|
1866
|
-
const byId = new Map(updates.map((
|
|
1866
|
+
const byId = new Map(updates.map((update2) => [update2.external_id, update2]));
|
|
1867
1867
|
const previous = [];
|
|
1868
1868
|
patchDocuments(qc, (old) => {
|
|
1869
1869
|
let changed = false;
|
|
1870
1870
|
const next = old.map((doc) => {
|
|
1871
|
-
const
|
|
1872
|
-
if (!
|
|
1871
|
+
const update2 = byId.get(doc.external_id);
|
|
1872
|
+
if (!update2) return doc;
|
|
1873
1873
|
changed = true;
|
|
1874
1874
|
previous.push(doc);
|
|
1875
|
-
return { ...doc, ...
|
|
1875
|
+
return { ...doc, ...update2 };
|
|
1876
1876
|
});
|
|
1877
1877
|
return changed ? next : old;
|
|
1878
1878
|
});
|
|
1879
|
-
const previousOwn = updates.map((
|
|
1879
|
+
const previousOwn = updates.map((update2) => {
|
|
1880
1880
|
const own = qc.getQueryData(
|
|
1881
|
-
arbiQueryKeys.document(
|
|
1881
|
+
arbiQueryKeys.document(update2.external_id)
|
|
1882
1882
|
);
|
|
1883
|
-
if (own) setCachedDocument(qc, { ...own, ...
|
|
1884
|
-
return [
|
|
1883
|
+
if (own) setCachedDocument(qc, { ...own, ...update2 });
|
|
1884
|
+
return [update2.external_id, own];
|
|
1885
1885
|
});
|
|
1886
1886
|
return { optimistic: true, previous, previousOwn };
|
|
1887
1887
|
}
|
|
@@ -2211,11 +2211,11 @@ function useUpdateDocTag() {
|
|
|
2211
2211
|
);
|
|
2212
2212
|
}
|
|
2213
2213
|
function queueProcessing(documents2) {
|
|
2214
|
-
const
|
|
2215
|
-
const previous = documents2.map(({ docId }) => [docId,
|
|
2214
|
+
const store3 = useArbiWsStore.getState();
|
|
2215
|
+
const previous = documents2.map(({ docId }) => [docId, store3.tasks[docId]]);
|
|
2216
2216
|
const now = Date.now();
|
|
2217
2217
|
for (const doc of documents2)
|
|
2218
|
-
|
|
2218
|
+
store3.putTask({
|
|
2219
2219
|
doc_ext_id: doc.docId,
|
|
2220
2220
|
workspace_ext_id: doc.workspaceId,
|
|
2221
2221
|
file_name: doc.fileName,
|
|
@@ -3992,14 +3992,14 @@ function watchPendingDocIds(queryClient) {
|
|
|
3992
3992
|
if (watched.has(queryClient)) return;
|
|
3993
3993
|
watched.add(queryClient);
|
|
3994
3994
|
const cache = queryClient.getMutationCache();
|
|
3995
|
-
const
|
|
3995
|
+
const update2 = () => {
|
|
3996
3996
|
const ids = new Set(
|
|
3997
3997
|
cache.findAll({ predicate: PENDING_SYNC_PREDICATE }).flatMap((m) => docIdsFromVariables(kindOf(m.options.mutationKey), m.state.variables))
|
|
3998
3998
|
);
|
|
3999
3999
|
if (!sameSet(usePendingDocIds.getState().ids, ids)) usePendingDocIds.setState({ ids });
|
|
4000
4000
|
};
|
|
4001
|
-
|
|
4002
|
-
cache.subscribe(
|
|
4001
|
+
update2();
|
|
4002
|
+
cache.subscribe(update2);
|
|
4003
4003
|
}
|
|
4004
4004
|
function useDocumentPendingSync(docId) {
|
|
4005
4005
|
const queryClient = useQueryClient();
|
|
@@ -4038,23 +4038,23 @@ function promisify(request) {
|
|
|
4038
4038
|
request.onerror = () => reject(request.error);
|
|
4039
4039
|
});
|
|
4040
4040
|
}
|
|
4041
|
-
async function withStore(mode,
|
|
4041
|
+
async function withStore(mode, run2) {
|
|
4042
4042
|
const db = await openDB();
|
|
4043
|
-
const
|
|
4044
|
-
return promisify(
|
|
4043
|
+
const store3 = db.transaction(STORE, mode).objectStore(STORE);
|
|
4044
|
+
return promisify(run2(store3));
|
|
4045
4045
|
}
|
|
4046
4046
|
async function putOfflineDocument(record) {
|
|
4047
|
-
await withStore("readwrite", (
|
|
4047
|
+
await withStore("readwrite", (store3) => store3.put(record));
|
|
4048
4048
|
}
|
|
4049
4049
|
async function getOfflineDocument(docExtId) {
|
|
4050
|
-
const record = await withStore("readonly", (
|
|
4050
|
+
const record = await withStore("readonly", (store3) => store3.get(docExtId));
|
|
4051
4051
|
return record;
|
|
4052
4052
|
}
|
|
4053
4053
|
async function deleteOfflineDocument(docExtId) {
|
|
4054
|
-
await withStore("readwrite", (
|
|
4054
|
+
await withStore("readwrite", (store3) => store3.delete(docExtId));
|
|
4055
4055
|
}
|
|
4056
4056
|
async function listOfflineDocumentIds() {
|
|
4057
|
-
const keys = await withStore("readonly", (
|
|
4057
|
+
const keys = await withStore("readonly", (store3) => store3.getAllKeys());
|
|
4058
4058
|
return keys;
|
|
4059
4059
|
}
|
|
4060
4060
|
async function requestPersistentStorage() {
|
|
@@ -5429,7 +5429,7 @@ function useAttachRun(conversationId, responseId) {
|
|
|
5429
5429
|
if (!conversationId || !responseId) return;
|
|
5430
5430
|
let disposed = false;
|
|
5431
5431
|
let controller = new AbortController();
|
|
5432
|
-
let
|
|
5432
|
+
let retryTimer2;
|
|
5433
5433
|
let reconnects = 0;
|
|
5434
5434
|
const runStartedAt = session.getState().messageTimers[`${conversationId}:${responseId}`]?.startTime;
|
|
5435
5435
|
session.setConversationGenerating(conversationId, true, null);
|
|
@@ -5454,7 +5454,7 @@ function useAttachRun(conversationId, responseId) {
|
|
|
5454
5454
|
if (isMessageRunning(queryClient, conversationId, responseId) && reconnects < MAX_RECONNECTS) {
|
|
5455
5455
|
reconnects += 1;
|
|
5456
5456
|
markLoading();
|
|
5457
|
-
|
|
5457
|
+
retryTimer2 = setTimeout(connect, RECONNECT_DELAY_MS);
|
|
5458
5458
|
return;
|
|
5459
5459
|
}
|
|
5460
5460
|
settle(false);
|
|
@@ -5540,7 +5540,7 @@ function useAttachRun(conversationId, responseId) {
|
|
|
5540
5540
|
const stop = () => {
|
|
5541
5541
|
if (disposed) return;
|
|
5542
5542
|
disposed = true;
|
|
5543
|
-
clearTimeout(
|
|
5543
|
+
clearTimeout(retryTimer2);
|
|
5544
5544
|
window.removeEventListener("online", reconnectWhenOnline);
|
|
5545
5545
|
controller.abort();
|
|
5546
5546
|
settle(true);
|
|
@@ -5549,7 +5549,7 @@ function useAttachRun(conversationId, responseId) {
|
|
|
5549
5549
|
return () => {
|
|
5550
5550
|
unregister();
|
|
5551
5551
|
disposed = true;
|
|
5552
|
-
clearTimeout(
|
|
5552
|
+
clearTimeout(retryTimer2);
|
|
5553
5553
|
window.removeEventListener("online", reconnectWhenOnline);
|
|
5554
5554
|
controller.abort();
|
|
5555
5555
|
settle(false);
|
|
@@ -6351,20 +6351,20 @@ function useConversationSettings() {
|
|
|
6351
6351
|
useEffect(() => {
|
|
6352
6352
|
if (isFullConfig(activeConfig)) session.setChatModelName(resolveConfigModel(activeConfig));
|
|
6353
6353
|
}, [activeConfig, configToLoad]);
|
|
6354
|
-
const
|
|
6355
|
-
const pending =
|
|
6354
|
+
const update2 = useUpdateConversation();
|
|
6355
|
+
const pending = update2.isPending ? update2.variables?.patch : void 0;
|
|
6356
6356
|
const saveTitle = useCallback(() => {
|
|
6357
6357
|
const trimmed = title.trim();
|
|
6358
6358
|
if (!conversationId || !isOwner || !trimmed || trimmed.length > TITLE_MAX) return;
|
|
6359
6359
|
if (trimmed === conversation?.title) return;
|
|
6360
|
-
|
|
6361
|
-
}, [conversationId, isOwner, title, conversation?.title,
|
|
6360
|
+
update2.mutate({ conversationId, patch: { title: trimmed } });
|
|
6361
|
+
}, [conversationId, isOwner, title, conversation?.title, update2]);
|
|
6362
6362
|
const setShared = useCallback(
|
|
6363
6363
|
(shared) => {
|
|
6364
6364
|
if (!conversationId || !isOwner || isShared === shared) return;
|
|
6365
|
-
|
|
6365
|
+
update2.mutate({ conversationId, patch: { shared } });
|
|
6366
6366
|
},
|
|
6367
|
-
[conversationId, isOwner, isShared,
|
|
6367
|
+
[conversationId, isOwner, isShared, update2]
|
|
6368
6368
|
);
|
|
6369
6369
|
const share = useCallback(() => setShared(true), [setShared]);
|
|
6370
6370
|
const unshare = useCallback(() => setShared(false), [setShared]);
|
|
@@ -6792,7 +6792,7 @@ function useOnlineSync() {
|
|
|
6792
6792
|
onlineManager.setOnline(computeOnline(wsStatus, signedIn2, workOffline, wsEverConnected.current));
|
|
6793
6793
|
}, [wsStatus, signedIn2, workOffline]);
|
|
6794
6794
|
useEffect(() => {
|
|
6795
|
-
const
|
|
6795
|
+
const update2 = () => onlineManager.setOnline(
|
|
6796
6796
|
computeOnline(
|
|
6797
6797
|
useArbiWsStore.getState().status,
|
|
6798
6798
|
signedInRef.current,
|
|
@@ -6800,11 +6800,11 @@ function useOnlineSync() {
|
|
|
6800
6800
|
wsEverConnected.current
|
|
6801
6801
|
)
|
|
6802
6802
|
);
|
|
6803
|
-
window.addEventListener("online",
|
|
6804
|
-
window.addEventListener("offline",
|
|
6803
|
+
window.addEventListener("online", update2);
|
|
6804
|
+
window.addEventListener("offline", update2);
|
|
6805
6805
|
return () => {
|
|
6806
|
-
window.removeEventListener("online",
|
|
6807
|
-
window.removeEventListener("offline",
|
|
6806
|
+
window.removeEventListener("online", update2);
|
|
6807
|
+
window.removeEventListener("offline", update2);
|
|
6808
6808
|
};
|
|
6809
6809
|
}, [session]);
|
|
6810
6810
|
}
|
|
@@ -6824,8 +6824,8 @@ function useReconnect() {
|
|
|
6824
6824
|
// src/websocket/reconnect.ts
|
|
6825
6825
|
var INITIAL_RECONNECT_DELAY_MS = 1e3;
|
|
6826
6826
|
var MAX_RECONNECT_DELAY_MS = 3e4;
|
|
6827
|
-
function nextReconnectDelay(
|
|
6828
|
-
return Math.min(INITIAL_RECONNECT_DELAY_MS * 2 ** Math.max(0,
|
|
6827
|
+
function nextReconnectDelay(attempt2) {
|
|
6828
|
+
return Math.min(INITIAL_RECONNECT_DELAY_MS * 2 ** Math.max(0, attempt2), MAX_RECONNECT_DELAY_MS);
|
|
6829
6829
|
}
|
|
6830
6830
|
function resolveSocketBaseUrl(configured, origin) {
|
|
6831
6831
|
return /^https?:\/\//.test(configured) || !origin ? configured : origin;
|
|
@@ -7110,13 +7110,13 @@ function useUpdateMailMessage() {
|
|
|
7110
7110
|
mutationFn: ({
|
|
7111
7111
|
accountId,
|
|
7112
7112
|
messageId,
|
|
7113
|
-
update
|
|
7113
|
+
update: update2
|
|
7114
7114
|
}) => mailJson(
|
|
7115
7115
|
client.fetch,
|
|
7116
7116
|
`${accountPath(accountId)}/messages/${encodeURIComponent(messageId)}`,
|
|
7117
7117
|
{
|
|
7118
7118
|
method: "PATCH",
|
|
7119
|
-
body: JSON.stringify(
|
|
7119
|
+
body: JSON.stringify(update2)
|
|
7120
7120
|
}
|
|
7121
7121
|
)
|
|
7122
7122
|
});
|
|
@@ -7438,15 +7438,15 @@ function ArbiWebSocketProvider({
|
|
|
7438
7438
|
}
|
|
7439
7439
|
}
|
|
7440
7440
|
};
|
|
7441
|
-
let
|
|
7442
|
-
let
|
|
7441
|
+
let retryTimer2 = null;
|
|
7442
|
+
let attempt2 = 0;
|
|
7443
7443
|
const scheduleRestart = () => {
|
|
7444
|
-
if (cancelled || evicted ||
|
|
7444
|
+
if (cancelled || evicted || retryTimer2) return;
|
|
7445
7445
|
setStatus("disconnected");
|
|
7446
|
-
const delay = nextReconnectDelay(
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7446
|
+
const delay = nextReconnectDelay(attempt2);
|
|
7447
|
+
attempt2 += 1;
|
|
7448
|
+
retryTimer2 = setTimeout(() => {
|
|
7449
|
+
retryTimer2 = null;
|
|
7450
7450
|
void start();
|
|
7451
7451
|
}, delay);
|
|
7452
7452
|
};
|
|
@@ -7477,7 +7477,7 @@ function ArbiWebSocketProvider({
|
|
|
7477
7477
|
onMessage,
|
|
7478
7478
|
onReconnecting: () => setStatus("reconnecting"),
|
|
7479
7479
|
onReconnected: () => {
|
|
7480
|
-
|
|
7480
|
+
attempt2 = 0;
|
|
7481
7481
|
setStatus("connected");
|
|
7482
7482
|
},
|
|
7483
7483
|
// Evicted (a newer session of the same user, or the server took this connection over for a
|
|
@@ -7499,7 +7499,7 @@ function ArbiWebSocketProvider({
|
|
|
7499
7499
|
return;
|
|
7500
7500
|
}
|
|
7501
7501
|
conn = c;
|
|
7502
|
-
|
|
7502
|
+
attempt2 = 0;
|
|
7503
7503
|
setStatus("connected");
|
|
7504
7504
|
} catch {
|
|
7505
7505
|
if (!info.signal.aborted) scheduleRestart();
|
|
@@ -7509,7 +7509,7 @@ function ArbiWebSocketProvider({
|
|
|
7509
7509
|
void start();
|
|
7510
7510
|
return () => {
|
|
7511
7511
|
cancelled = true;
|
|
7512
|
-
if (
|
|
7512
|
+
if (retryTimer2) clearTimeout(retryTimer2);
|
|
7513
7513
|
conn?.close();
|
|
7514
7514
|
reset();
|
|
7515
7515
|
};
|
|
@@ -7584,10 +7584,10 @@ function createWorkspaceSwitch({
|
|
|
7584
7584
|
queryClient
|
|
7585
7585
|
}) {
|
|
7586
7586
|
const switchWorkspace = (target) => {
|
|
7587
|
-
const
|
|
7588
|
-
switching.add(
|
|
7589
|
-
void
|
|
7590
|
-
return
|
|
7587
|
+
const run2 = runSwitch(target);
|
|
7588
|
+
switching.add(run2);
|
|
7589
|
+
void run2.finally(() => switching.delete(run2));
|
|
7590
|
+
return run2;
|
|
7591
7591
|
};
|
|
7592
7592
|
const runSwitch = async (target) => {
|
|
7593
7593
|
const { workspaceId: previousId, workspaceStatus } = session.getState();
|
|
@@ -8254,7 +8254,7 @@ function useFactsSearch(options = {}) {
|
|
|
8254
8254
|
const workspaceId = options.workspaceId ?? (arbi.hasWorkspace ? arbi.getWorkspaceId() : null);
|
|
8255
8255
|
const [kept, setKept] = useTransientView(FACTS_SEARCH_VIEW, EMPTY2);
|
|
8256
8256
|
const searchFacts = useSearchFacts(workspaceId);
|
|
8257
|
-
const
|
|
8257
|
+
const run2 = useRef(0);
|
|
8258
8258
|
const foreign = !!kept.workspaceId && !!workspaceId && kept.workspaceId !== workspaceId;
|
|
8259
8259
|
const state = foreign ? EMPTY2 : kept;
|
|
8260
8260
|
const patch = useCallback(
|
|
@@ -8269,18 +8269,18 @@ function useFactsSearch(options = {}) {
|
|
|
8269
8269
|
const search = useCallback(async () => {
|
|
8270
8270
|
const q = query.trim();
|
|
8271
8271
|
if (!q) return;
|
|
8272
|
-
const mine = ++
|
|
8272
|
+
const mine = ++run2.current;
|
|
8273
8273
|
try {
|
|
8274
8274
|
const res = await searchFacts.mutateAsync({ query: q, search_mode: mode, limit });
|
|
8275
|
-
if (
|
|
8275
|
+
if (run2.current !== mine) return;
|
|
8276
8276
|
patch({ hits: res.facts, lastQuery: q });
|
|
8277
8277
|
} catch (error) {
|
|
8278
|
-
if (
|
|
8278
|
+
if (run2.current !== mine) return;
|
|
8279
8279
|
throw error;
|
|
8280
8280
|
}
|
|
8281
8281
|
}, [query, searchFacts, mode, limit, patch]);
|
|
8282
8282
|
const clear = useCallback(() => {
|
|
8283
|
-
|
|
8283
|
+
run2.current++;
|
|
8284
8284
|
patch({ query: "", hits: null });
|
|
8285
8285
|
}, [patch]);
|
|
8286
8286
|
return {
|
|
@@ -8498,26 +8498,26 @@ var DEFAULT_LIMIT = 50;
|
|
|
8498
8498
|
var pad = (n) => String(n).padStart(2, "0");
|
|
8499
8499
|
function asDay(value) {
|
|
8500
8500
|
if (typeof value !== "string" || value.length < 10) return null;
|
|
8501
|
-
const
|
|
8502
|
-
return Number.isNaN(
|
|
8501
|
+
const day2 = /* @__PURE__ */ new Date(`${value.slice(0, 10)}T00:00:00Z`);
|
|
8502
|
+
return Number.isNaN(day2.getTime()) ? null : day2;
|
|
8503
8503
|
}
|
|
8504
|
-
function isoWeek(
|
|
8505
|
-
const d = new Date(
|
|
8504
|
+
function isoWeek(day2) {
|
|
8505
|
+
const d = new Date(day2);
|
|
8506
8506
|
const weekday = d.getUTCDay() || 7;
|
|
8507
8507
|
d.setUTCDate(d.getUTCDate() + 4 - weekday);
|
|
8508
8508
|
const yearStart = Date.UTC(d.getUTCFullYear(), 0, 1);
|
|
8509
8509
|
return [d.getUTCFullYear(), Math.ceil(((d.getTime() - yearStart) / 864e5 + 1) / 7)];
|
|
8510
8510
|
}
|
|
8511
|
-
function bucketOf(value,
|
|
8512
|
-
const
|
|
8513
|
-
if (!
|
|
8514
|
-
const y =
|
|
8515
|
-
const m =
|
|
8516
|
-
switch (
|
|
8511
|
+
function bucketOf(value, bucket2) {
|
|
8512
|
+
const day2 = asDay(value);
|
|
8513
|
+
if (!day2) return null;
|
|
8514
|
+
const y = day2.getUTCFullYear();
|
|
8515
|
+
const m = day2.getUTCMonth() + 1;
|
|
8516
|
+
switch (bucket2) {
|
|
8517
8517
|
case "day":
|
|
8518
|
-
return `${y}-${pad(m)}-${pad(
|
|
8518
|
+
return `${y}-${pad(m)}-${pad(day2.getUTCDate())}`;
|
|
8519
8519
|
case "week": {
|
|
8520
|
-
const [wy, w] = isoWeek(
|
|
8520
|
+
const [wy, w] = isoWeek(day2);
|
|
8521
8521
|
return `${wy}-W${pad(w)}`;
|
|
8522
8522
|
}
|
|
8523
8523
|
case "quarter":
|
|
@@ -8528,12 +8528,12 @@ function bucketOf(value, bucket) {
|
|
|
8528
8528
|
return `${y}-${pad(m)}`;
|
|
8529
8529
|
}
|
|
8530
8530
|
}
|
|
8531
|
-
function bucketStart(key,
|
|
8531
|
+
function bucketStart(key, bucket2) {
|
|
8532
8532
|
const parts = key.match(/^(\d{4})(?:-(W|Q)?(\d{1,2}))?(?:-(\d{2}))?$/);
|
|
8533
8533
|
if (!parts) return null;
|
|
8534
8534
|
const year = Number(parts[1]);
|
|
8535
8535
|
const n = Number(parts[3] ?? 1);
|
|
8536
|
-
switch (
|
|
8536
|
+
switch (bucket2) {
|
|
8537
8537
|
case "day":
|
|
8538
8538
|
return asDay(key);
|
|
8539
8539
|
case "week": {
|
|
@@ -8550,32 +8550,32 @@ function bucketStart(key, bucket) {
|
|
|
8550
8550
|
return new Date(Date.UTC(year, n - 1, 1));
|
|
8551
8551
|
}
|
|
8552
8552
|
}
|
|
8553
|
-
function nextBucket(
|
|
8554
|
-
const d = new Date(
|
|
8555
|
-
if (
|
|
8556
|
-
else if (
|
|
8557
|
-
else if (
|
|
8558
|
-
else if (
|
|
8553
|
+
function nextBucket(day2, bucket2) {
|
|
8554
|
+
const d = new Date(day2);
|
|
8555
|
+
if (bucket2 === "day") d.setUTCDate(d.getUTCDate() + 1);
|
|
8556
|
+
else if (bucket2 === "week") d.setUTCDate(d.getUTCDate() + 7);
|
|
8557
|
+
else if (bucket2 === "month") d.setUTCMonth(d.getUTCMonth() + 1);
|
|
8558
|
+
else if (bucket2 === "quarter") d.setUTCMonth(d.getUTCMonth() + 3);
|
|
8559
8559
|
else d.setUTCFullYear(d.getUTCFullYear() + 1);
|
|
8560
8560
|
return d;
|
|
8561
8561
|
}
|
|
8562
|
-
function bucketRange(first, last,
|
|
8563
|
-
const start = bucketStart(first,
|
|
8564
|
-
const end = bucketStart(last,
|
|
8562
|
+
function bucketRange(first, last, bucket2, max = 500) {
|
|
8563
|
+
const start = bucketStart(first, bucket2);
|
|
8564
|
+
const end = bucketStart(last, bucket2);
|
|
8565
8565
|
if (!start || !end || start > end) return null;
|
|
8566
8566
|
const keys = [];
|
|
8567
|
-
for (let d = start; d <= end && keys.length <= max; d = nextBucket(d,
|
|
8568
|
-
keys.push(bucketOf(d.toISOString(),
|
|
8567
|
+
for (let d = start; d <= end && keys.length <= max; d = nextBucket(d, bucket2)) {
|
|
8568
|
+
keys.push(bucketOf(d.toISOString(), bucket2));
|
|
8569
8569
|
}
|
|
8570
8570
|
return keys.length > max ? null : keys;
|
|
8571
8571
|
}
|
|
8572
8572
|
var isEmpty = (value) => value === null || value === void 0 || value === "" || Array.isArray(value) && !value.length;
|
|
8573
|
-
function groupKeys(value, field,
|
|
8573
|
+
function groupKeys(value, field, bucket2) {
|
|
8574
8574
|
if (isEmpty(value)) return [""];
|
|
8575
8575
|
if (Array.isArray(value)) {
|
|
8576
|
-
return [...new Set(value.map((v) => groupKeys(v, field,
|
|
8576
|
+
return [...new Set(value.map((v) => groupKeys(v, field, bucket2)[0]))];
|
|
8577
8577
|
}
|
|
8578
|
-
if (field.type === "date") return [bucketOf(value,
|
|
8578
|
+
if (field.type === "date") return [bucketOf(value, bucket2) ?? ""];
|
|
8579
8579
|
if (typeof value === "boolean") return [value ? "Yes" : "No"];
|
|
8580
8580
|
return [String(value)];
|
|
8581
8581
|
}
|
|
@@ -8670,7 +8670,7 @@ function aggregateDataset(dataset, query) {
|
|
|
8670
8670
|
const split = query.splitBy ? byId.get(query.splitBy) : void 0;
|
|
8671
8671
|
const op = query.measure?.op ?? "count";
|
|
8672
8672
|
const valueField = query.measure?.field ? byId.get(query.measure.field) : void 0;
|
|
8673
|
-
const
|
|
8673
|
+
const bucket2 = query.bucket ?? "month";
|
|
8674
8674
|
const fields = aggregateFields(dataset.fields, query);
|
|
8675
8675
|
if (op !== "count" && valueField?.type !== "number") {
|
|
8676
8676
|
throw new Error(`${op} needs a number field`);
|
|
@@ -8678,8 +8678,8 @@ function aggregateDataset(dataset, query) {
|
|
|
8678
8678
|
const rows = filterRows(dataset.rows, dataset.fields, query.filters);
|
|
8679
8679
|
const cells = /* @__PURE__ */ new Map();
|
|
8680
8680
|
for (const row of rows) {
|
|
8681
|
-
const groups = group ? groupKeys(row[group.id], group,
|
|
8682
|
-
const splits = split ? groupKeys(row[split.id], split,
|
|
8681
|
+
const groups = group ? groupKeys(row[group.id], group, bucket2) : ["All"];
|
|
8682
|
+
const splits = split ? groupKeys(row[split.id], split, bucket2) : [""];
|
|
8683
8683
|
const value = valueField ? row[valueField.id] ?? null : 1;
|
|
8684
8684
|
for (const g of groups) {
|
|
8685
8685
|
const bySplit = cells.get(g) ?? /* @__PURE__ */ new Map();
|
|
@@ -8725,12 +8725,12 @@ function aggregateDataset(dataset, query) {
|
|
|
8725
8725
|
}
|
|
8726
8726
|
return { fields, rows: out, totalRows: rows.length, truncated: keys.length > limit };
|
|
8727
8727
|
}
|
|
8728
|
-
function fillTimeGaps(dataset,
|
|
8728
|
+
function fillTimeGaps(dataset, bucket2) {
|
|
8729
8729
|
const keyId = AGGREGATE_FIELDS.key;
|
|
8730
8730
|
const keys = [...new Set(dataset.rows.map((r) => String(r[keyId] ?? "")))].filter(Boolean);
|
|
8731
8731
|
if (keys.length < 2) return dataset;
|
|
8732
8732
|
const sorted = [...keys].sort();
|
|
8733
|
-
const range = bucketRange(sorted[0], sorted[sorted.length - 1],
|
|
8733
|
+
const range = bucketRange(sorted[0], sorted[sorted.length - 1], bucket2);
|
|
8734
8734
|
if (!range || range.length === keys.length) return dataset;
|
|
8735
8735
|
const hasSeries = dataset.fields.some((f) => f.id === AGGREGATE_FIELDS.series);
|
|
8736
8736
|
const seriesLabels = hasSeries ? [...new Set(dataset.rows.map((r) => String(r[AGGREGATE_FIELDS.series])))] : [null];
|
|
@@ -9272,8 +9272,8 @@ var DEFAULT_USAGE_SOURCE_LABELS = {
|
|
|
9272
9272
|
var monthsBackOf = (params) => String(params.period ?? "0") === "1" ? 1 : 0;
|
|
9273
9273
|
function usageRows(usage, project, modelLabel = (g) => MODEL_GROUP_LABELS[g] ?? g) {
|
|
9274
9274
|
return (usage.daily ?? []).flatMap(
|
|
9275
|
-
(
|
|
9276
|
-
day:
|
|
9275
|
+
(day2) => Object.entries(day2.model_groups ?? {}).map(([group, mg]) => ({
|
|
9276
|
+
day: day2.date,
|
|
9277
9277
|
project,
|
|
9278
9278
|
model: modelLabel(group),
|
|
9279
9279
|
spend: mg.spend ?? 0,
|
|
@@ -9537,18 +9537,18 @@ function useDashboardStore() {
|
|
|
9537
9537
|
const { dashboards } = useArbiHost();
|
|
9538
9538
|
return (dashboards?.useStore ?? useDatabaseDashboardStore)();
|
|
9539
9539
|
}
|
|
9540
|
-
function useSavedDashboards(
|
|
9540
|
+
function useSavedDashboards(store3) {
|
|
9541
9541
|
return useQuery({
|
|
9542
|
-
queryKey: arbiQueryKeys.dashboards(
|
|
9543
|
-
queryFn: () =>
|
|
9544
|
-
enabled:
|
|
9542
|
+
queryKey: arbiQueryKeys.dashboards(store3.scope ?? ""),
|
|
9543
|
+
queryFn: () => store3.list(),
|
|
9544
|
+
enabled: store3.scope !== null
|
|
9545
9545
|
});
|
|
9546
9546
|
}
|
|
9547
|
-
function useSaveDashboard(
|
|
9547
|
+
function useSaveDashboard(store3) {
|
|
9548
9548
|
const queryClient = useQueryClient();
|
|
9549
|
-
const key = arbiQueryKeys.dashboards(
|
|
9549
|
+
const key = arbiQueryKeys.dashboards(store3.scope ?? "");
|
|
9550
9550
|
return useMutation({
|
|
9551
|
-
mutationFn: (dashboard) =>
|
|
9551
|
+
mutationFn: (dashboard) => store3.save(dashboard),
|
|
9552
9552
|
onSuccess: (saved) => {
|
|
9553
9553
|
queryClient.setQueryData(
|
|
9554
9554
|
key,
|
|
@@ -9558,11 +9558,11 @@ function useSaveDashboard(store) {
|
|
|
9558
9558
|
}
|
|
9559
9559
|
});
|
|
9560
9560
|
}
|
|
9561
|
-
function useDeleteDashboard(
|
|
9561
|
+
function useDeleteDashboard(store3) {
|
|
9562
9562
|
const queryClient = useQueryClient();
|
|
9563
|
-
const key = arbiQueryKeys.dashboards(
|
|
9563
|
+
const key = arbiQueryKeys.dashboards(store3.scope ?? "");
|
|
9564
9564
|
return useMutation({
|
|
9565
|
-
mutationFn: (id) =>
|
|
9565
|
+
mutationFn: (id) => store3.remove(id),
|
|
9566
9566
|
onSuccess: (_void, id) => {
|
|
9567
9567
|
queryClient.setQueryData(key, (old = []) => old.filter((d) => d.id !== id));
|
|
9568
9568
|
void queryClient.invalidateQueries({ queryKey: arbiQueryKeys.databaseRows() });
|
|
@@ -9665,8 +9665,8 @@ function isClientError(error) {
|
|
|
9665
9665
|
}
|
|
9666
9666
|
return false;
|
|
9667
9667
|
}
|
|
9668
|
-
function retryDelay(
|
|
9669
|
-
return Math.min(1e3 * 2 **
|
|
9668
|
+
function retryDelay(attempt2) {
|
|
9669
|
+
return Math.min(1e3 * 2 ** attempt2, 3e4) + Math.random() * 500;
|
|
9670
9670
|
}
|
|
9671
9671
|
function shouldRetryQuery(failureCount, error) {
|
|
9672
9672
|
return failureCount < 2 && !isClientError(error) && !isAbortError(error);
|
|
@@ -9707,6 +9707,14 @@ function createArbiQueryClient({
|
|
|
9707
9707
|
});
|
|
9708
9708
|
return { queryClient, persistOptions: persist ? createArbiQueryPersistOptions() : null };
|
|
9709
9709
|
}
|
|
9710
|
+
|
|
9711
|
+
// src/offline-store/network.ts
|
|
9712
|
+
function isNetworkFailure(error) {
|
|
9713
|
+
return error instanceof TypeError && /fetch|network|load failed|internet/i.test(error.message);
|
|
9714
|
+
}
|
|
9715
|
+
function isBrowserOffline() {
|
|
9716
|
+
return typeof navigator !== "undefined" && navigator.onLine === false;
|
|
9717
|
+
}
|
|
9710
9718
|
function useFilteredProjects(filters, options) {
|
|
9711
9719
|
const arbi = useArbi();
|
|
9712
9720
|
return useQuery({
|
|
@@ -10924,7 +10932,7 @@ function useIdentityVerificationFlow(api, {
|
|
|
10924
10932
|
useEffect(() => {
|
|
10925
10933
|
if (status && lastVerified.current === void 0) lastVerified.current = status.verified;
|
|
10926
10934
|
}, [status]);
|
|
10927
|
-
const
|
|
10935
|
+
const run2 = useCallback(
|
|
10928
10936
|
async (kind, fallback, fn) => {
|
|
10929
10937
|
setBusy(kind);
|
|
10930
10938
|
setProblem(null);
|
|
@@ -10945,7 +10953,7 @@ function useIdentityVerificationFlow(api, {
|
|
|
10945
10953
|
const startVerifyRef = useRef(async () => {
|
|
10946
10954
|
});
|
|
10947
10955
|
const refresh = useCallback(async () => {
|
|
10948
|
-
const next = await
|
|
10956
|
+
const next = await run2(
|
|
10949
10957
|
"refresh",
|
|
10950
10958
|
"Could not read the verification result",
|
|
10951
10959
|
() => apiRef.current.refresh()
|
|
@@ -10957,7 +10965,7 @@ function useIdentityVerificationFlow(api, {
|
|
|
10957
10965
|
const nextStep = next.state === "declined" ? "result" : identityStepFor(next);
|
|
10958
10966
|
if (nextStep === "verify" && isOpenRef.current) void startVerifyRef.current();
|
|
10959
10967
|
else setStep(nextStep);
|
|
10960
|
-
}, [
|
|
10968
|
+
}, [run2, apply]);
|
|
10961
10969
|
const refreshedOnLoad = useRef(false);
|
|
10962
10970
|
useEffect(() => {
|
|
10963
10971
|
if (refreshedOnLoad.current || !status) return;
|
|
@@ -10967,7 +10975,7 @@ function useIdentityVerificationFlow(api, {
|
|
|
10967
10975
|
const startVerify = useCallback(async () => {
|
|
10968
10976
|
setStep("verify");
|
|
10969
10977
|
setEmbedFailed(false);
|
|
10970
|
-
const url = await
|
|
10978
|
+
const url = await run2(
|
|
10971
10979
|
"start",
|
|
10972
10980
|
"Could not start identity verification",
|
|
10973
10981
|
() => apiRef.current.startSession()
|
|
@@ -10977,7 +10985,7 @@ function useIdentityVerificationFlow(api, {
|
|
|
10977
10985
|
return;
|
|
10978
10986
|
}
|
|
10979
10987
|
setStep("pay");
|
|
10980
|
-
}, [
|
|
10988
|
+
}, [run2]);
|
|
10981
10989
|
startVerifyRef.current = startVerify;
|
|
10982
10990
|
useEffect(() => {
|
|
10983
10991
|
if (!isOpen || step !== "verify" || !sessionUrl || !container || embedFailed) return;
|
|
@@ -11032,7 +11040,7 @@ function useIdentityVerificationFlow(api, {
|
|
|
11032
11040
|
}, []);
|
|
11033
11041
|
closeRef.current = close;
|
|
11034
11042
|
const pay = useCallback(async () => {
|
|
11035
|
-
const result = await
|
|
11043
|
+
const result = await run2(
|
|
11036
11044
|
"pay",
|
|
11037
11045
|
"Payment could not be completed",
|
|
11038
11046
|
() => apiRef.current.purchase()
|
|
@@ -11045,10 +11053,10 @@ function useIdentityVerificationFlow(api, {
|
|
|
11045
11053
|
setPaymentUrl(null);
|
|
11046
11054
|
void query.refetch();
|
|
11047
11055
|
void startVerify();
|
|
11048
|
-
}, [
|
|
11056
|
+
}, [run2, query, startVerify]);
|
|
11049
11057
|
const acceptName = useCallback(async () => {
|
|
11050
11058
|
const offered = status;
|
|
11051
|
-
const next = await
|
|
11059
|
+
const next = await run2(
|
|
11052
11060
|
"accept",
|
|
11053
11061
|
"Could not change your name",
|
|
11054
11062
|
() => apiRef.current.acceptName()
|
|
@@ -11061,15 +11069,15 @@ function useIdentityVerificationFlow(api, {
|
|
|
11061
11069
|
familyName: offered.document_family_name ?? null
|
|
11062
11070
|
});
|
|
11063
11071
|
}
|
|
11064
|
-
}, [
|
|
11072
|
+
}, [run2, apply, status, onNameAccepted]);
|
|
11065
11073
|
const remove = useCallback(async () => {
|
|
11066
|
-
const next = await
|
|
11074
|
+
const next = await run2(
|
|
11067
11075
|
"remove",
|
|
11068
11076
|
"Could not remove the verification",
|
|
11069
11077
|
() => apiRef.current.remove()
|
|
11070
11078
|
);
|
|
11071
11079
|
if (next) apply(next);
|
|
11072
|
-
}, [
|
|
11080
|
+
}, [run2, apply]);
|
|
11073
11081
|
const price = useMemo(() => status ? formatPrice(status) : null, [status, formatPrice]);
|
|
11074
11082
|
return {
|
|
11075
11083
|
status,
|
|
@@ -11124,6 +11132,1221 @@ async function downloadRecoveryFile(params) {
|
|
|
11124
11132
|
});
|
|
11125
11133
|
downloadBlob(new Blob([file.html], { type: "text/html" }), file.filename);
|
|
11126
11134
|
}
|
|
11135
|
+
async function documentMailAttachment(arbi, doc, maxBytes, version) {
|
|
11136
|
+
const older = version && version !== (doc.current_version ?? 1) ? version : null;
|
|
11137
|
+
if (!onlineManager.isOnline() && !older) return offlineDocumentAttachment(doc, maxBytes);
|
|
11138
|
+
const failed = (cause) => new Error(`Could not download ${getDocumentTitle(doc, "the document")}`, { cause });
|
|
11139
|
+
const response = await arbi.documents.download(doc.external_id, older ?? void 0).catch((error) => {
|
|
11140
|
+
throw failed(error);
|
|
11141
|
+
});
|
|
11142
|
+
if (!response.ok) throw failed();
|
|
11143
|
+
const blob = await response.blob();
|
|
11144
|
+
if (blob.size > maxBytes) throw new Error("Document exceeds the email attachment limit");
|
|
11145
|
+
const fileName = older ? doc.versions?.find((v) => v.n === older)?.file_name ?? doc.file_name : doc.file_name;
|
|
11146
|
+
return fileAttachment(
|
|
11147
|
+
new File([blob], fileName || getDocumentTitle(doc, "document"), { type: blob.type })
|
|
11148
|
+
);
|
|
11149
|
+
}
|
|
11150
|
+
async function offlineDocumentAttachment(doc, maxBytes) {
|
|
11151
|
+
const title = getDocumentTitle(doc, "This document");
|
|
11152
|
+
const record = await getOfflineDocument(doc.external_id);
|
|
11153
|
+
const isPdf = /\.pdf$/i.test(doc.file_name ?? "");
|
|
11154
|
+
if (!record || record.kind === "pdf" && !isPdf)
|
|
11155
|
+
throw new Error(
|
|
11156
|
+
`${title} isn't available offline. Make it available offline first, or attach it when you're back online.`
|
|
11157
|
+
);
|
|
11158
|
+
const blob = await loadOfflineDocument(doc.external_id);
|
|
11159
|
+
if (!blob) throw new Error(`${title} isn't available offline.`);
|
|
11160
|
+
if (blob.size > maxBytes) throw new Error("Document exceeds the email attachment limit");
|
|
11161
|
+
return fileAttachment(
|
|
11162
|
+
new File([blob], doc.file_name || getDocumentTitle(doc, "document"), { type: blob.type })
|
|
11163
|
+
);
|
|
11164
|
+
}
|
|
11165
|
+
var OfflineMailLockedError = class extends Error {
|
|
11166
|
+
constructor() {
|
|
11167
|
+
super("Sign in to open the mail saved on this device");
|
|
11168
|
+
this.name = "OfflineMailLockedError";
|
|
11169
|
+
}
|
|
11170
|
+
};
|
|
11171
|
+
var sealer = createOfflineSealer("arbi-offline-mail-v1", {
|
|
11172
|
+
locked: () => new OfflineMailLockedError()
|
|
11173
|
+
});
|
|
11174
|
+
var forgetOfflineMailKeys = sealer.forget;
|
|
11175
|
+
var sealBytes = sealer.sealBytes;
|
|
11176
|
+
var openBytes = sealer.openBytes;
|
|
11177
|
+
var sealJson = sealer.sealJson;
|
|
11178
|
+
var openJson = sealer.openJson;
|
|
11179
|
+
var blind = sealer.blind;
|
|
11180
|
+
|
|
11181
|
+
// src/offline-store/store.ts
|
|
11182
|
+
var DB_VERSION2 = 1;
|
|
11183
|
+
var RECORDS = "records";
|
|
11184
|
+
var META = "meta";
|
|
11185
|
+
function promisify2(request) {
|
|
11186
|
+
return new Promise((resolve, reject) => {
|
|
11187
|
+
request.onsuccess = () => resolve(request.result);
|
|
11188
|
+
request.onerror = () => reject(request.error);
|
|
11189
|
+
});
|
|
11190
|
+
}
|
|
11191
|
+
function createRecordStore(dbName) {
|
|
11192
|
+
let dbPromise = null;
|
|
11193
|
+
function openDB2() {
|
|
11194
|
+
if (dbPromise) return dbPromise;
|
|
11195
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
11196
|
+
const request = indexedDB.open(dbName, DB_VERSION2);
|
|
11197
|
+
request.onerror = () => {
|
|
11198
|
+
dbPromise = null;
|
|
11199
|
+
reject(request.error);
|
|
11200
|
+
};
|
|
11201
|
+
request.onsuccess = () => {
|
|
11202
|
+
const db = request.result;
|
|
11203
|
+
db.onversionchange = () => {
|
|
11204
|
+
db.close();
|
|
11205
|
+
dbPromise = null;
|
|
11206
|
+
};
|
|
11207
|
+
resolve(db);
|
|
11208
|
+
};
|
|
11209
|
+
request.onupgradeneeded = () => {
|
|
11210
|
+
const db = request.result;
|
|
11211
|
+
if (!db.objectStoreNames.contains(RECORDS)) {
|
|
11212
|
+
const records = db.createObjectStore(RECORDS, { keyPath: "key" });
|
|
11213
|
+
records.createIndex("bucket", "bucket");
|
|
11214
|
+
records.createIndex("kind", "kind");
|
|
11215
|
+
}
|
|
11216
|
+
if (!db.objectStoreNames.contains(META)) db.createObjectStore(META, { keyPath: "key" });
|
|
11217
|
+
};
|
|
11218
|
+
});
|
|
11219
|
+
return dbPromise;
|
|
11220
|
+
}
|
|
11221
|
+
async function run2(store3, mode, fn) {
|
|
11222
|
+
const db = await openDB2();
|
|
11223
|
+
return promisify2(fn(db.transaction(store3, mode).objectStore(store3)));
|
|
11224
|
+
}
|
|
11225
|
+
async function scan(pick) {
|
|
11226
|
+
const db = await openDB2();
|
|
11227
|
+
const store3 = db.transaction(RECORDS, "readonly").objectStore(RECORDS);
|
|
11228
|
+
return new Promise((resolve, reject) => {
|
|
11229
|
+
const out = [];
|
|
11230
|
+
const cursor = store3.openCursor();
|
|
11231
|
+
cursor.onerror = () => reject(cursor.error);
|
|
11232
|
+
cursor.onsuccess = () => {
|
|
11233
|
+
const c = cursor.result;
|
|
11234
|
+
if (!c) return resolve(out);
|
|
11235
|
+
out.push(pick(c.value));
|
|
11236
|
+
c.continue();
|
|
11237
|
+
};
|
|
11238
|
+
});
|
|
11239
|
+
}
|
|
11240
|
+
const self = {
|
|
11241
|
+
dbName,
|
|
11242
|
+
async putRecord(record) {
|
|
11243
|
+
await run2(RECORDS, "readwrite", (s) => s.put(record));
|
|
11244
|
+
},
|
|
11245
|
+
async getRecord(key) {
|
|
11246
|
+
return await run2(RECORDS, "readonly", (s) => s.get(key));
|
|
11247
|
+
},
|
|
11248
|
+
async hasRecord(key) {
|
|
11249
|
+
return await run2(RECORDS, "readonly", (s) => s.count(key)) > 0;
|
|
11250
|
+
},
|
|
11251
|
+
async deleteRecords(keys) {
|
|
11252
|
+
if (keys.length === 0) return;
|
|
11253
|
+
const db = await openDB2();
|
|
11254
|
+
const tx = db.transaction(RECORDS, "readwrite");
|
|
11255
|
+
const store3 = tx.objectStore(RECORDS);
|
|
11256
|
+
for (const key of keys) store3.delete(key);
|
|
11257
|
+
await new Promise((resolve, reject) => {
|
|
11258
|
+
tx.oncomplete = () => resolve();
|
|
11259
|
+
tx.onerror = () => reject(tx.error);
|
|
11260
|
+
tx.onabort = () => reject(tx.error);
|
|
11261
|
+
});
|
|
11262
|
+
},
|
|
11263
|
+
async recordsInBucket(bucket2) {
|
|
11264
|
+
return await run2(
|
|
11265
|
+
RECORDS,
|
|
11266
|
+
"readonly",
|
|
11267
|
+
(s) => s.index("bucket").getAll(bucket2)
|
|
11268
|
+
);
|
|
11269
|
+
},
|
|
11270
|
+
async recordsOfKind(kind) {
|
|
11271
|
+
return await run2(
|
|
11272
|
+
RECORDS,
|
|
11273
|
+
"readonly",
|
|
11274
|
+
(s) => s.index("kind").getAll(kind)
|
|
11275
|
+
);
|
|
11276
|
+
},
|
|
11277
|
+
allRecordSizes() {
|
|
11278
|
+
return scan(({ key, kind, size }) => ({ key, kind, size }));
|
|
11279
|
+
},
|
|
11280
|
+
allRecordAccess() {
|
|
11281
|
+
return scan(({ key, kind, lastAccess }) => ({ key, kind, lastAccess }));
|
|
11282
|
+
},
|
|
11283
|
+
async touchRecord(key) {
|
|
11284
|
+
const record = await self.getRecord(key);
|
|
11285
|
+
if (record) await self.putRecord({ ...record, lastAccess: Date.now() });
|
|
11286
|
+
},
|
|
11287
|
+
async getMeta(key) {
|
|
11288
|
+
const row = await run2(META, "readonly", (s) => s.get(key));
|
|
11289
|
+
return row?.value;
|
|
11290
|
+
},
|
|
11291
|
+
async setMeta(key, value) {
|
|
11292
|
+
await run2(META, "readwrite", (s) => s.put({ key, value }));
|
|
11293
|
+
},
|
|
11294
|
+
async deleteDatabase() {
|
|
11295
|
+
const db = await dbPromise?.catch(() => null);
|
|
11296
|
+
db?.close();
|
|
11297
|
+
dbPromise = null;
|
|
11298
|
+
await new Promise((resolve, reject) => {
|
|
11299
|
+
const request = indexedDB.deleteDatabase(dbName);
|
|
11300
|
+
request.onsuccess = () => resolve();
|
|
11301
|
+
request.onerror = () => reject(request.error);
|
|
11302
|
+
request.onblocked = () => {
|
|
11303
|
+
};
|
|
11304
|
+
});
|
|
11305
|
+
}
|
|
11306
|
+
};
|
|
11307
|
+
return self;
|
|
11308
|
+
}
|
|
11309
|
+
|
|
11310
|
+
// src/mail/offline/store.ts
|
|
11311
|
+
var OFFLINE_MAIL_DB = "arbi-offline-mail";
|
|
11312
|
+
var store = createRecordStore(OFFLINE_MAIL_DB);
|
|
11313
|
+
var putRecord = store.putRecord;
|
|
11314
|
+
var getRecord = store.getRecord;
|
|
11315
|
+
var hasRecord = store.hasRecord;
|
|
11316
|
+
var deleteRecords = store.deleteRecords;
|
|
11317
|
+
var recordsInBucket = store.recordsInBucket;
|
|
11318
|
+
var recordsOfKind = store.recordsOfKind;
|
|
11319
|
+
var allRecordSizes = store.allRecordSizes;
|
|
11320
|
+
var touchRecord = store.touchRecord;
|
|
11321
|
+
var getMeta = store.getMeta;
|
|
11322
|
+
var setMeta = store.setMeta;
|
|
11323
|
+
var deleteOfflineMailDatabase = store.deleteDatabase;
|
|
11324
|
+
|
|
11325
|
+
// src/mail/offline/mailbox.ts
|
|
11326
|
+
var keyOf = {
|
|
11327
|
+
accounts: () => blind("accounts"),
|
|
11328
|
+
policy: () => blind("policy"),
|
|
11329
|
+
folders: (accountId) => blind("folders", accountId),
|
|
11330
|
+
header: (accountId, folderId, messageId) => blind("header", accountId, folderId, messageId),
|
|
11331
|
+
body: (accountId, messageId) => blind("body", accountId, messageId),
|
|
11332
|
+
attachment: (accountId, messageId, attachmentId) => blind("attachment", accountId, messageId, attachmentId)
|
|
11333
|
+
};
|
|
11334
|
+
var bucketOf2 = {
|
|
11335
|
+
global: () => blind("bucket", "global"),
|
|
11336
|
+
folder: (accountId, folderId) => blind("bucket", "folder", accountId, folderId),
|
|
11337
|
+
account: (accountId) => blind("bucket", "account", accountId)
|
|
11338
|
+
};
|
|
11339
|
+
async function put(kind, key, bucket2, data, sortAt = Date.now()) {
|
|
11340
|
+
const now = Date.now();
|
|
11341
|
+
await putRecord({ key, kind, bucket: bucket2, sortAt, size: data.byteLength, lastAccess: now, data });
|
|
11342
|
+
}
|
|
11343
|
+
async function readJson(record) {
|
|
11344
|
+
if (!record) return void 0;
|
|
11345
|
+
try {
|
|
11346
|
+
return await openJson(record.data);
|
|
11347
|
+
} catch {
|
|
11348
|
+
return void 0;
|
|
11349
|
+
}
|
|
11350
|
+
}
|
|
11351
|
+
var receivedAt = (m) => m.received_at ? Date.parse(m.received_at) || 0 : 0;
|
|
11352
|
+
async function saveAccounts(accounts) {
|
|
11353
|
+
await put("accounts", await keyOf.accounts(), await bucketOf2.global(), await sealJson(accounts));
|
|
11354
|
+
}
|
|
11355
|
+
async function loadAccounts() {
|
|
11356
|
+
return readJson(await getRecord(await keyOf.accounts()));
|
|
11357
|
+
}
|
|
11358
|
+
async function savePolicy(policy) {
|
|
11359
|
+
await put("policy", await keyOf.policy(), await bucketOf2.global(), await sealJson(policy));
|
|
11360
|
+
}
|
|
11361
|
+
async function loadPolicy() {
|
|
11362
|
+
return readJson(await getRecord(await keyOf.policy()));
|
|
11363
|
+
}
|
|
11364
|
+
async function saveFolders(accountId, folders) {
|
|
11365
|
+
await put(
|
|
11366
|
+
"folders",
|
|
11367
|
+
await keyOf.folders(accountId),
|
|
11368
|
+
await bucketOf2.account(accountId),
|
|
11369
|
+
await sealJson(folders)
|
|
11370
|
+
);
|
|
11371
|
+
}
|
|
11372
|
+
async function loadFolders(accountId) {
|
|
11373
|
+
return readJson(await getRecord(await keyOf.folders(accountId)));
|
|
11374
|
+
}
|
|
11375
|
+
function toHeader(message) {
|
|
11376
|
+
return {
|
|
11377
|
+
...message,
|
|
11378
|
+
text: "",
|
|
11379
|
+
html: void 0,
|
|
11380
|
+
delivery: void 0,
|
|
11381
|
+
attachments: message.attachments.map((a) => ({ ...a, content_base64: null }))
|
|
11382
|
+
};
|
|
11383
|
+
}
|
|
11384
|
+
async function saveHeader(accountId, folderId, message) {
|
|
11385
|
+
await put(
|
|
11386
|
+
"header",
|
|
11387
|
+
await keyOf.header(accountId, folderId, message.id),
|
|
11388
|
+
await bucketOf2.folder(accountId, folderId),
|
|
11389
|
+
await sealJson(toHeader(message)),
|
|
11390
|
+
receivedAt(message)
|
|
11391
|
+
);
|
|
11392
|
+
}
|
|
11393
|
+
async function loadHeaders(accountId, folderId) {
|
|
11394
|
+
const records = await recordsInBucket(await bucketOf2.folder(accountId, folderId));
|
|
11395
|
+
const out = [];
|
|
11396
|
+
for (const record of records) {
|
|
11397
|
+
if (record.kind !== "header") continue;
|
|
11398
|
+
const message = await readJson(record);
|
|
11399
|
+
if (message) out.push({ key: record.key, sortAt: record.sortAt, message });
|
|
11400
|
+
}
|
|
11401
|
+
return out.sort((a, b) => b.sortAt - a.sortAt);
|
|
11402
|
+
}
|
|
11403
|
+
async function hasFolder(accountId) {
|
|
11404
|
+
return hasRecord(await keyOf.folders(accountId));
|
|
11405
|
+
}
|
|
11406
|
+
async function saveBody(accountId, message) {
|
|
11407
|
+
const stripped = {
|
|
11408
|
+
...message,
|
|
11409
|
+
attachments: message.attachments.map((a) => ({ ...a, content_base64: null }))
|
|
11410
|
+
};
|
|
11411
|
+
await put(
|
|
11412
|
+
"body",
|
|
11413
|
+
await keyOf.body(accountId, message.id),
|
|
11414
|
+
await bucketOf2.account(accountId),
|
|
11415
|
+
await sealJson(stripped),
|
|
11416
|
+
receivedAt(message)
|
|
11417
|
+
);
|
|
11418
|
+
}
|
|
11419
|
+
async function hasBody(accountId, messageId) {
|
|
11420
|
+
return hasRecord(await keyOf.body(accountId, messageId));
|
|
11421
|
+
}
|
|
11422
|
+
async function loadBody(accountId, messageId) {
|
|
11423
|
+
return readJson(await getRecord(await keyOf.body(accountId, messageId)));
|
|
11424
|
+
}
|
|
11425
|
+
async function saveAttachment(accountId, messageId, attachmentId, bytes) {
|
|
11426
|
+
await put(
|
|
11427
|
+
"attachment",
|
|
11428
|
+
await keyOf.attachment(accountId, messageId, attachmentId),
|
|
11429
|
+
await bucketOf2.account(accountId),
|
|
11430
|
+
await sealBytes(bytes)
|
|
11431
|
+
);
|
|
11432
|
+
}
|
|
11433
|
+
async function hasAttachment(accountId, messageId, attachmentId) {
|
|
11434
|
+
return hasRecord(await keyOf.attachment(accountId, messageId, attachmentId));
|
|
11435
|
+
}
|
|
11436
|
+
async function loadAttachment(accountId, messageId, attachmentId) {
|
|
11437
|
+
const key = await keyOf.attachment(accountId, messageId, attachmentId);
|
|
11438
|
+
const record = await getRecord(key);
|
|
11439
|
+
if (!record) return void 0;
|
|
11440
|
+
try {
|
|
11441
|
+
const bytes = await openBytes(record.data);
|
|
11442
|
+
void touchRecord(key);
|
|
11443
|
+
return bytes;
|
|
11444
|
+
} catch {
|
|
11445
|
+
return void 0;
|
|
11446
|
+
}
|
|
11447
|
+
}
|
|
11448
|
+
async function removeKeys(keys) {
|
|
11449
|
+
await deleteRecords(keys);
|
|
11450
|
+
}
|
|
11451
|
+
function bodyText(body) {
|
|
11452
|
+
const html = body.text ? "" : (body.html ?? "").replace(/<[^>]*>/g, " ");
|
|
11453
|
+
return `${body.text} ${html} ${body.attachments.map((a) => a.name).join(" ")}`;
|
|
11454
|
+
}
|
|
11455
|
+
var folded = (value) => value.toLocaleLowerCase().normalize("NFKD");
|
|
11456
|
+
async function searchFolder(accountId, folderId, query) {
|
|
11457
|
+
const words = folded(query).split(/\s+/).filter(Boolean);
|
|
11458
|
+
const headers = await loadHeaders(accountId, folderId);
|
|
11459
|
+
if (words.length === 0) return headers.map((h) => h.message);
|
|
11460
|
+
const out = [];
|
|
11461
|
+
for (const { message } of headers) {
|
|
11462
|
+
const people = [...message.sender, ...message.to, ...message.cc].map((p) => `${p.name ?? ""} ${p.email}`).join(" ");
|
|
11463
|
+
let haystack = folded(`${message.subject} ${people} ${message.preview}`);
|
|
11464
|
+
if (!words.every((w) => haystack.includes(w))) {
|
|
11465
|
+
const body = await loadBody(accountId, message.id);
|
|
11466
|
+
if (body) haystack += " " + folded(bodyText(body));
|
|
11467
|
+
}
|
|
11468
|
+
if (words.every((w) => haystack.includes(w))) out.push(message);
|
|
11469
|
+
}
|
|
11470
|
+
return out;
|
|
11471
|
+
}
|
|
11472
|
+
|
|
11473
|
+
// src/mail/offline/settings.ts
|
|
11474
|
+
var DEFAULT_OFFLINE_MAIL_SETTINGS = {
|
|
11475
|
+
enabled: false,
|
|
11476
|
+
scope: "90d",
|
|
11477
|
+
attachmentCapMb: 25,
|
|
11478
|
+
quotaMb: 1024
|
|
11479
|
+
};
|
|
11480
|
+
var SCOPE_DAYS = {
|
|
11481
|
+
"30d": 30,
|
|
11482
|
+
"90d": 90,
|
|
11483
|
+
"365d": 365,
|
|
11484
|
+
all: null
|
|
11485
|
+
};
|
|
11486
|
+
var STORAGE_PREFIX = "arbi-offline-mail-settings:";
|
|
11487
|
+
function getOfflineMailSettings(userExtId) {
|
|
11488
|
+
if (!userExtId) return DEFAULT_OFFLINE_MAIL_SETTINGS;
|
|
11489
|
+
const key = STORAGE_PREFIX + userExtId;
|
|
11490
|
+
try {
|
|
11491
|
+
const raw = localStorage.getItem(key);
|
|
11492
|
+
return raw ? { ...DEFAULT_OFFLINE_MAIL_SETTINGS, ...JSON.parse(raw) } : DEFAULT_OFFLINE_MAIL_SETTINGS;
|
|
11493
|
+
} catch {
|
|
11494
|
+
return DEFAULT_OFFLINE_MAIL_SETTINGS;
|
|
11495
|
+
}
|
|
11496
|
+
}
|
|
11497
|
+
function setOfflineMailSettings(userExtId, patch) {
|
|
11498
|
+
const next = { ...getOfflineMailSettings(userExtId), ...patch };
|
|
11499
|
+
if (userExtId) {
|
|
11500
|
+
try {
|
|
11501
|
+
localStorage.setItem(STORAGE_PREFIX + userExtId, JSON.stringify(next));
|
|
11502
|
+
} catch {
|
|
11503
|
+
}
|
|
11504
|
+
}
|
|
11505
|
+
notifyOfflineMailChange("settings");
|
|
11506
|
+
return next;
|
|
11507
|
+
}
|
|
11508
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
11509
|
+
var CHANNEL = "arbi-offline-mail";
|
|
11510
|
+
var channel = null;
|
|
11511
|
+
function getChannel() {
|
|
11512
|
+
if (channel || typeof BroadcastChannel === "undefined") return channel;
|
|
11513
|
+
channel = new BroadcastChannel(CHANNEL);
|
|
11514
|
+
channel.onmessage = (event) => {
|
|
11515
|
+
for (const listener of listeners) listener(event.data);
|
|
11516
|
+
};
|
|
11517
|
+
return channel;
|
|
11518
|
+
}
|
|
11519
|
+
function notifyOfflineMailChange(change) {
|
|
11520
|
+
for (const listener of listeners) listener(change);
|
|
11521
|
+
try {
|
|
11522
|
+
getChannel()?.postMessage(change);
|
|
11523
|
+
} catch {
|
|
11524
|
+
}
|
|
11525
|
+
}
|
|
11526
|
+
function onOfflineMailChange(listener) {
|
|
11527
|
+
getChannel();
|
|
11528
|
+
listeners.add(listener);
|
|
11529
|
+
return () => listeners.delete(listener);
|
|
11530
|
+
}
|
|
11531
|
+
|
|
11532
|
+
// src/mail/offline/drafts.ts
|
|
11533
|
+
var keyFor = (id) => blind("draft", id);
|
|
11534
|
+
async function saveLocalDraft(accountId, compose, id = crypto.randomUUID()) {
|
|
11535
|
+
const draft = { id, accountId, compose, updatedAt: Date.now() };
|
|
11536
|
+
await putRecord({
|
|
11537
|
+
key: await keyFor(id),
|
|
11538
|
+
kind: "draft",
|
|
11539
|
+
bucket: await blind("bucket", "drafts", accountId),
|
|
11540
|
+
sortAt: draft.updatedAt,
|
|
11541
|
+
size: 0,
|
|
11542
|
+
lastAccess: draft.updatedAt,
|
|
11543
|
+
data: await sealJson(draft)
|
|
11544
|
+
});
|
|
11545
|
+
notifyOfflineMailChange("drafts");
|
|
11546
|
+
return draft;
|
|
11547
|
+
}
|
|
11548
|
+
async function listLocalDrafts(accountId) {
|
|
11549
|
+
const drafts = [];
|
|
11550
|
+
for (const record of await recordsOfKind("draft")) {
|
|
11551
|
+
try {
|
|
11552
|
+
const draft = await openJson(record.data);
|
|
11553
|
+
if (!accountId || draft.accountId === accountId) drafts.push(draft);
|
|
11554
|
+
} catch {
|
|
11555
|
+
}
|
|
11556
|
+
}
|
|
11557
|
+
return drafts.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
11558
|
+
}
|
|
11559
|
+
async function deleteLocalDraft(id) {
|
|
11560
|
+
await deleteRecords([await keyFor(id)]);
|
|
11561
|
+
notifyOfflineMailChange("drafts");
|
|
11562
|
+
}
|
|
11563
|
+
async function syncLocalDrafts(save) {
|
|
11564
|
+
let uploaded = 0;
|
|
11565
|
+
for (const draft of await listLocalDrafts()) {
|
|
11566
|
+
if (draft.error) continue;
|
|
11567
|
+
try {
|
|
11568
|
+
await save(draft);
|
|
11569
|
+
await deleteLocalDraft(draft.id);
|
|
11570
|
+
uploaded++;
|
|
11571
|
+
} catch (error) {
|
|
11572
|
+
const status = error.status;
|
|
11573
|
+
if (isNetworkFailure(error) || status === void 0 || status === 401 || status >= 500) break;
|
|
11574
|
+
await putRecord({
|
|
11575
|
+
key: await keyFor(draft.id),
|
|
11576
|
+
kind: "draft",
|
|
11577
|
+
bucket: await blind("bucket", "drafts", draft.accountId),
|
|
11578
|
+
sortAt: draft.updatedAt,
|
|
11579
|
+
size: 0,
|
|
11580
|
+
lastAccess: Date.now(),
|
|
11581
|
+
data: await sealJson({
|
|
11582
|
+
...draft,
|
|
11583
|
+
error: error instanceof Error ? error.message : "The server refused this draft"
|
|
11584
|
+
})
|
|
11585
|
+
});
|
|
11586
|
+
notifyOfflineMailChange("drafts");
|
|
11587
|
+
}
|
|
11588
|
+
}
|
|
11589
|
+
return uploaded;
|
|
11590
|
+
}
|
|
11591
|
+
|
|
11592
|
+
// src/mail/offline/outbox.ts
|
|
11593
|
+
var bucket = () => blind("bucket", "outbox");
|
|
11594
|
+
var keyFor2 = (id) => blind("outbox", id);
|
|
11595
|
+
function outboxProblem(compose) {
|
|
11596
|
+
const missing = compose.attachments.find((a) => a.content_base64 == null);
|
|
11597
|
+
if (missing)
|
|
11598
|
+
return `${missing.name} isn't saved on this device, so it can't be sent offline. Remove it or send once you're back online.`;
|
|
11599
|
+
return null;
|
|
11600
|
+
}
|
|
11601
|
+
var seqCounter = 0;
|
|
11602
|
+
async function write(item) {
|
|
11603
|
+
await putRecord({
|
|
11604
|
+
key: await keyFor2(item.id),
|
|
11605
|
+
kind: "outbox",
|
|
11606
|
+
bucket: await bucket(),
|
|
11607
|
+
sortAt: item.seq,
|
|
11608
|
+
size: 0,
|
|
11609
|
+
lastAccess: Date.now(),
|
|
11610
|
+
data: await sealJson(item)
|
|
11611
|
+
});
|
|
11612
|
+
}
|
|
11613
|
+
async function listOutbox() {
|
|
11614
|
+
const records = await recordsInBucket(await bucket());
|
|
11615
|
+
const items = [];
|
|
11616
|
+
for (const record of records) {
|
|
11617
|
+
try {
|
|
11618
|
+
items.push(await openJson(record.data));
|
|
11619
|
+
} catch {
|
|
11620
|
+
}
|
|
11621
|
+
}
|
|
11622
|
+
return items.sort((a, b) => a.seq - b.seq);
|
|
11623
|
+
}
|
|
11624
|
+
async function enqueueOutbox(input) {
|
|
11625
|
+
const problem = outboxProblem(input.compose);
|
|
11626
|
+
if (problem) throw new Error(problem);
|
|
11627
|
+
const now = Date.now();
|
|
11628
|
+
seqCounter = Math.max(seqCounter + 1, now * 1e3);
|
|
11629
|
+
const item = {
|
|
11630
|
+
id: crypto.randomUUID(),
|
|
11631
|
+
requestId: input.requestId,
|
|
11632
|
+
account: input.account,
|
|
11633
|
+
compose: input.compose,
|
|
11634
|
+
secure: input.secure,
|
|
11635
|
+
securePayload: input.securePayload,
|
|
11636
|
+
status: "queued",
|
|
11637
|
+
attempts: 0,
|
|
11638
|
+
createdAt: now,
|
|
11639
|
+
seq: seqCounter
|
|
11640
|
+
};
|
|
11641
|
+
await write(item);
|
|
11642
|
+
notifyOfflineMailChange("outbox");
|
|
11643
|
+
return item;
|
|
11644
|
+
}
|
|
11645
|
+
async function update(item, patch) {
|
|
11646
|
+
const next = { ...item, ...patch };
|
|
11647
|
+
await write(next);
|
|
11648
|
+
notifyOfflineMailChange("outbox");
|
|
11649
|
+
return next;
|
|
11650
|
+
}
|
|
11651
|
+
async function deleteOutboxItem(id) {
|
|
11652
|
+
await deleteRecords([await keyFor2(id)]);
|
|
11653
|
+
notifyOfflineMailChange("outbox");
|
|
11654
|
+
}
|
|
11655
|
+
async function retryOutboxItem(id) {
|
|
11656
|
+
const item = (await listOutbox()).find((i) => i.id === id);
|
|
11657
|
+
if (item && item.status === "failed")
|
|
11658
|
+
await update(item, { status: "queued", error: void 0, inDoubt: item.inDoubt });
|
|
11659
|
+
}
|
|
11660
|
+
var IN_DOUBT_MESSAGE = "The mail server didn't confirm this message. It may already have been sent: check Sent before sending it again.";
|
|
11661
|
+
function classify(error) {
|
|
11662
|
+
if (isNetworkFailure(error)) return { kind: "offline" };
|
|
11663
|
+
const status = error?.status;
|
|
11664
|
+
const detail = error instanceof Error ? error.message : "The message could not be sent";
|
|
11665
|
+
if (status === void 0) return { kind: "failed", error: detail, inDoubt: false };
|
|
11666
|
+
if (status === 409) return { kind: "failed", error: IN_DOUBT_MESSAGE, inDoubt: true };
|
|
11667
|
+
if (status >= 500) return { kind: "failed", error: IN_DOUBT_MESSAGE, inDoubt: true };
|
|
11668
|
+
return { kind: "failed", error: detail, inDoubt: false };
|
|
11669
|
+
}
|
|
11670
|
+
async function prepared(item, sender) {
|
|
11671
|
+
if (!item.secure || item.securePayload !== void 0) return item;
|
|
11672
|
+
return update(item, { securePayload: await sender.prepareSecure(item) });
|
|
11673
|
+
}
|
|
11674
|
+
async function attempt(item, sender) {
|
|
11675
|
+
try {
|
|
11676
|
+
const result = await sender.send(item);
|
|
11677
|
+
return { kind: "sent", serverId: result?.id };
|
|
11678
|
+
} catch (error) {
|
|
11679
|
+
const outcome = classify(error);
|
|
11680
|
+
if (error.status === 401) {
|
|
11681
|
+
try {
|
|
11682
|
+
await sender.unlock(item.account);
|
|
11683
|
+
const result = await sender.send(item);
|
|
11684
|
+
return { kind: "sent", serverId: result?.id };
|
|
11685
|
+
} catch (retryError) {
|
|
11686
|
+
return classify(retryError);
|
|
11687
|
+
}
|
|
11688
|
+
}
|
|
11689
|
+
return outcome;
|
|
11690
|
+
}
|
|
11691
|
+
}
|
|
11692
|
+
async function flushOutboxOnce(sender, isOnline) {
|
|
11693
|
+
const result = { sent: 0, failed: 0, interrupted: false };
|
|
11694
|
+
const unlocked2 = /* @__PURE__ */ new Set();
|
|
11695
|
+
for (; ; ) {
|
|
11696
|
+
if (!isOnline()) {
|
|
11697
|
+
result.interrupted = true;
|
|
11698
|
+
break;
|
|
11699
|
+
}
|
|
11700
|
+
const next = (await listOutbox()).find((i) => i.status === "queued" || i.status === "sending");
|
|
11701
|
+
if (!next) break;
|
|
11702
|
+
let item = await update(next, { status: "sending", attempts: next.attempts + 1 });
|
|
11703
|
+
try {
|
|
11704
|
+
item = await prepared(item, sender);
|
|
11705
|
+
} catch (error) {
|
|
11706
|
+
const message = error instanceof Error ? error.message : "Could not encrypt the message";
|
|
11707
|
+
await update(item, { status: "failed", error: message });
|
|
11708
|
+
result.failed++;
|
|
11709
|
+
continue;
|
|
11710
|
+
}
|
|
11711
|
+
if (!unlocked2.has(item.account.id)) {
|
|
11712
|
+
await sender.unlock(item.account).catch(() => {
|
|
11713
|
+
});
|
|
11714
|
+
unlocked2.add(item.account.id);
|
|
11715
|
+
}
|
|
11716
|
+
const outcome = await attempt(item, sender);
|
|
11717
|
+
if (outcome.kind === "sent") {
|
|
11718
|
+
item = await update(item, {
|
|
11719
|
+
status: "sent",
|
|
11720
|
+
sentAt: Date.now(),
|
|
11721
|
+
error: void 0,
|
|
11722
|
+
inDoubt: false
|
|
11723
|
+
});
|
|
11724
|
+
result.sent++;
|
|
11725
|
+
} else if (outcome.kind === "offline") {
|
|
11726
|
+
await update(item, { status: "queued" });
|
|
11727
|
+
result.interrupted = true;
|
|
11728
|
+
break;
|
|
11729
|
+
} else {
|
|
11730
|
+
await update(item, {
|
|
11731
|
+
status: "failed",
|
|
11732
|
+
error: outcome.error,
|
|
11733
|
+
inDoubt: item.inDoubt || outcome.inDoubt
|
|
11734
|
+
});
|
|
11735
|
+
result.failed++;
|
|
11736
|
+
}
|
|
11737
|
+
}
|
|
11738
|
+
return result;
|
|
11739
|
+
}
|
|
11740
|
+
async function purgeSentOutbox(olderThanMs = 0) {
|
|
11741
|
+
const cutoff = Date.now() - olderThanMs;
|
|
11742
|
+
const done = (await listOutbox()).filter((i) => i.status === "sent" && (i.sentAt ?? 0) <= cutoff);
|
|
11743
|
+
if (done.length === 0) return;
|
|
11744
|
+
await deleteRecords(await Promise.all(done.map((i) => keyFor2(i.id))));
|
|
11745
|
+
notifyOfflineMailChange("outbox");
|
|
11746
|
+
}
|
|
11747
|
+
var STATUS_KEY = "status";
|
|
11748
|
+
var FULL_SYNC_EVERY_MS = 60 * 60 * 1e3;
|
|
11749
|
+
var MAX_PAGES = 200;
|
|
11750
|
+
async function getOfflineMailStatus() {
|
|
11751
|
+
return await getMeta(STATUS_KEY) ?? { messages: 0, bytes: 0, skipped: [] };
|
|
11752
|
+
}
|
|
11753
|
+
var MB = 1024 * 1024;
|
|
11754
|
+
var time = (m) => m.received_at ? Date.parse(m.received_at) || 0 : 0;
|
|
11755
|
+
var sameHeader = (a, b) => JSON.stringify(toHeader(a)) === JSON.stringify(toHeader(b));
|
|
11756
|
+
var Budget = class {
|
|
11757
|
+
constructor(used, quota) {
|
|
11758
|
+
this.used = used;
|
|
11759
|
+
this.quota = quota;
|
|
11760
|
+
}
|
|
11761
|
+
used;
|
|
11762
|
+
quota;
|
|
11763
|
+
get full() {
|
|
11764
|
+
return this.used >= this.quota;
|
|
11765
|
+
}
|
|
11766
|
+
take(bytes) {
|
|
11767
|
+
this.used += bytes;
|
|
11768
|
+
}
|
|
11769
|
+
};
|
|
11770
|
+
async function saveMessageContent(transport, account, header, settings, budget) {
|
|
11771
|
+
const message = await transport.message(account, header.id);
|
|
11772
|
+
await saveBody(account.id, message);
|
|
11773
|
+
budget.take((message.text?.length ?? 0) + (message.html?.length ?? 0));
|
|
11774
|
+
let attachmentBytes = 0;
|
|
11775
|
+
for (const attachment of message.attachments) {
|
|
11776
|
+
if (budget.full) return;
|
|
11777
|
+
if (attachmentBytes + attachment.size > settings.attachmentCapMb * MB) continue;
|
|
11778
|
+
if (await hasAttachment(account.id, message.id, attachment.id)) continue;
|
|
11779
|
+
const bytes = attachment.content_base64 ? Uint8Array.from(atob(attachment.content_base64), (c) => c.charCodeAt(0)) : await transport.attachment(account, message.id, attachment.id);
|
|
11780
|
+
await saveAttachment(account.id, message.id, attachment.id, bytes);
|
|
11781
|
+
attachmentBytes += bytes.byteLength;
|
|
11782
|
+
budget.take(bytes.byteLength);
|
|
11783
|
+
}
|
|
11784
|
+
}
|
|
11785
|
+
async function syncFolder(transport, account, folder, settings, windowStart, full, budget) {
|
|
11786
|
+
const saved = new Map(
|
|
11787
|
+
(await loadHeaders(account.id, folder.id)).map((h) => [h.message.id, h])
|
|
11788
|
+
);
|
|
11789
|
+
const seen = /* @__PURE__ */ new Set();
|
|
11790
|
+
let oldestSeen = Infinity;
|
|
11791
|
+
let cursor = null;
|
|
11792
|
+
let reachedEnd = false;
|
|
11793
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
11794
|
+
const result = await transport.messages(account, folder.id, cursor);
|
|
11795
|
+
let anythingNew = false;
|
|
11796
|
+
let outOfScope = false;
|
|
11797
|
+
for (const message of result.messages) {
|
|
11798
|
+
const at = time(message);
|
|
11799
|
+
if (at && at < windowStart) {
|
|
11800
|
+
outOfScope = true;
|
|
11801
|
+
break;
|
|
11802
|
+
}
|
|
11803
|
+
seen.add(message.id);
|
|
11804
|
+
if (at) oldestSeen = Math.min(oldestSeen, at);
|
|
11805
|
+
const previous = saved.get(message.id)?.message;
|
|
11806
|
+
if (!previous || !sameHeader(previous, message)) {
|
|
11807
|
+
await saveHeader(account.id, folder.id, message);
|
|
11808
|
+
anythingNew = true;
|
|
11809
|
+
}
|
|
11810
|
+
if (!budget.full && !await hasBody(account.id, message.id)) {
|
|
11811
|
+
try {
|
|
11812
|
+
await saveMessageContent(transport, account, message, settings, budget);
|
|
11813
|
+
anythingNew = true;
|
|
11814
|
+
} catch (error) {
|
|
11815
|
+
logger.warn("[offline-mail] could not save a message body", error);
|
|
11816
|
+
}
|
|
11817
|
+
}
|
|
11818
|
+
}
|
|
11819
|
+
cursor = result.next_cursor ?? null;
|
|
11820
|
+
if (outOfScope || !cursor) {
|
|
11821
|
+
reachedEnd = true;
|
|
11822
|
+
break;
|
|
11823
|
+
}
|
|
11824
|
+
if (!full && !anythingNew) break;
|
|
11825
|
+
}
|
|
11826
|
+
const stale = [...saved.values()].filter(
|
|
11827
|
+
(h) => !seen.has(h.message.id) && (reachedEnd || h.sortAt >= oldestSeen)
|
|
11828
|
+
);
|
|
11829
|
+
await removeKeys(stale.map((h) => h.key));
|
|
11830
|
+
}
|
|
11831
|
+
async function collectGarbage(accounts) {
|
|
11832
|
+
for (const account of accounts) {
|
|
11833
|
+
const folders = await loadFolders(account.id) ?? [];
|
|
11834
|
+
const expected = /* @__PURE__ */ new Set([await keyOf.folders(account.id)]);
|
|
11835
|
+
for (const folder of folders) {
|
|
11836
|
+
for (const { message } of await loadHeaders(account.id, folder.id)) {
|
|
11837
|
+
expected.add(await keyOf.body(account.id, message.id));
|
|
11838
|
+
for (const attachment of message.attachments)
|
|
11839
|
+
expected.add(await keyOf.attachment(account.id, message.id, attachment.id));
|
|
11840
|
+
}
|
|
11841
|
+
}
|
|
11842
|
+
const orphans = (await recordsInBucket(await bucketOf2.account(account.id))).filter((r) => (r.kind === "body" || r.kind === "attachment") && !expected.has(r.key)).map((r) => r.key);
|
|
11843
|
+
await removeKeys(orphans);
|
|
11844
|
+
}
|
|
11845
|
+
}
|
|
11846
|
+
async function forgetAccount(accountId) {
|
|
11847
|
+
for (const folder of await loadFolders(accountId) ?? []) {
|
|
11848
|
+
const records2 = await recordsInBucket(await bucketOf2.folder(accountId, folder.id));
|
|
11849
|
+
await removeKeys(records2.map((r) => r.key));
|
|
11850
|
+
}
|
|
11851
|
+
const records = await recordsInBucket(await bucketOf2.account(accountId));
|
|
11852
|
+
await removeKeys(records.filter((r) => r.kind !== "draft").map((r) => r.key));
|
|
11853
|
+
}
|
|
11854
|
+
async function enforceQuota(quotaBytes) {
|
|
11855
|
+
let used = (await allRecordSizes()).reduce((n, r) => n + r.size, 0);
|
|
11856
|
+
if (used <= quotaBytes) return used;
|
|
11857
|
+
for (const kind of ["attachment", "body"]) {
|
|
11858
|
+
const records = (await recordsOfKind(kind)).sort(
|
|
11859
|
+
(a, b) => kind === "attachment" ? a.lastAccess - b.lastAccess : a.sortAt - b.sortAt
|
|
11860
|
+
);
|
|
11861
|
+
const evict = [];
|
|
11862
|
+
for (const record of records) {
|
|
11863
|
+
if (used <= quotaBytes) break;
|
|
11864
|
+
evict.push(record.key);
|
|
11865
|
+
used -= record.size;
|
|
11866
|
+
}
|
|
11867
|
+
await removeKeys(evict);
|
|
11868
|
+
if (used <= quotaBytes) break;
|
|
11869
|
+
}
|
|
11870
|
+
return used;
|
|
11871
|
+
}
|
|
11872
|
+
async function syncMailbox(transport, settings, options = {}) {
|
|
11873
|
+
const now = options.now ?? Date.now();
|
|
11874
|
+
const previous = await getOfflineMailStatus();
|
|
11875
|
+
const full = options.full || !previous.lastFullSyncAt || now - previous.lastFullSyncAt > FULL_SYNC_EVERY_MS;
|
|
11876
|
+
const days = SCOPE_DAYS[settings.scope];
|
|
11877
|
+
const windowStart = days === null ? 0 : now - days * 24 * 60 * 60 * 1e3;
|
|
11878
|
+
const quota = settings.quotaMb * MB;
|
|
11879
|
+
const skipped = [];
|
|
11880
|
+
let error;
|
|
11881
|
+
try {
|
|
11882
|
+
await savePolicy(await transport.policy());
|
|
11883
|
+
const accounts = await transport.accounts();
|
|
11884
|
+
const before = await loadAccounts() ?? [];
|
|
11885
|
+
await saveAccounts(accounts);
|
|
11886
|
+
for (const gone of before.filter((b) => !accounts.some((a) => a.id === b.id)))
|
|
11887
|
+
await forgetAccount(gone.id);
|
|
11888
|
+
const budget = new Budget(
|
|
11889
|
+
(await allRecordSizes()).reduce((n, r) => n + r.size, 0),
|
|
11890
|
+
quota
|
|
11891
|
+
);
|
|
11892
|
+
for (const account of accounts) {
|
|
11893
|
+
if (account.needs_reconsent) {
|
|
11894
|
+
skipped.push(account.email);
|
|
11895
|
+
continue;
|
|
11896
|
+
}
|
|
11897
|
+
try {
|
|
11898
|
+
await transport.unlock(account);
|
|
11899
|
+
const folders = await transport.folders(account);
|
|
11900
|
+
const oldFolders = await loadFolders(account.id) ?? [];
|
|
11901
|
+
await saveFolders(account.id, folders);
|
|
11902
|
+
for (const gone of oldFolders.filter((o) => !folders.some((f) => f.id === o.id))) {
|
|
11903
|
+
const records = await recordsInBucket(await bucketOf2.folder(account.id, gone.id));
|
|
11904
|
+
await removeKeys(records.map((r) => r.key));
|
|
11905
|
+
}
|
|
11906
|
+
for (const folder of folders)
|
|
11907
|
+
await syncFolder(transport, account, folder, settings, windowStart, full, budget);
|
|
11908
|
+
notifyOfflineMailChange("mailbox");
|
|
11909
|
+
} catch (accountError) {
|
|
11910
|
+
if (accountError.status === void 0) throw accountError;
|
|
11911
|
+
logger.warn("[offline-mail] account sync failed", accountError);
|
|
11912
|
+
skipped.push(account.email);
|
|
11913
|
+
}
|
|
11914
|
+
}
|
|
11915
|
+
if (full) await collectGarbage(accounts);
|
|
11916
|
+
} catch (syncError) {
|
|
11917
|
+
error = syncError.status === void 0 ? "The connection dropped during sync. It will continue when you are back online." : syncError instanceof Error ? syncError.message : "Sync failed";
|
|
11918
|
+
}
|
|
11919
|
+
const bytes = await enforceQuota(quota);
|
|
11920
|
+
const messages = (await recordsOfKind("header")).length;
|
|
11921
|
+
const status = {
|
|
11922
|
+
lastSyncedAt: error ? previous.lastSyncedAt : now,
|
|
11923
|
+
lastFullSyncAt: !error && full ? now : previous.lastFullSyncAt,
|
|
11924
|
+
messages,
|
|
11925
|
+
bytes,
|
|
11926
|
+
error,
|
|
11927
|
+
skipped
|
|
11928
|
+
};
|
|
11929
|
+
await setMeta(STATUS_KEY, status);
|
|
11930
|
+
notifyOfflineMailChange("status");
|
|
11931
|
+
return status;
|
|
11932
|
+
}
|
|
11933
|
+
|
|
11934
|
+
// src/mail/offline/live.ts
|
|
11935
|
+
var OUTBOX_FOLDER = "outbox";
|
|
11936
|
+
function sessionUser(arbi) {
|
|
11937
|
+
return arbi.session?.getState().user?.external_id ?? null;
|
|
11938
|
+
}
|
|
11939
|
+
function hasLiveSession(arbi) {
|
|
11940
|
+
return !!arbi.session?.getState().serverSessionKey;
|
|
11941
|
+
}
|
|
11942
|
+
function canReachMail(arbi) {
|
|
11943
|
+
return onlineManager.isOnline() && hasLiveSession(arbi);
|
|
11944
|
+
}
|
|
11945
|
+
var unlocked = { nativeUnlocked: true };
|
|
11946
|
+
function mailCall(arbi, path, init = {}) {
|
|
11947
|
+
return mailJson((p, i) => arbi.mail.fetch(p, i, unlocked), path, init);
|
|
11948
|
+
}
|
|
11949
|
+
function liveTransport(arbi) {
|
|
11950
|
+
const unlock = async (account) => {
|
|
11951
|
+
if (!hasLiveSession(arbi))
|
|
11952
|
+
throw Object.assign(new Error("Sign in to unlock your mailbox"), { status: 401 });
|
|
11953
|
+
await arbi.mail.unlockAccount(account);
|
|
11954
|
+
};
|
|
11955
|
+
return {
|
|
11956
|
+
policy: () => mailCall(arbi, "/policy"),
|
|
11957
|
+
accounts: () => mailCall(arbi, "/accounts"),
|
|
11958
|
+
unlock,
|
|
11959
|
+
folders: (account) => mailCall(arbi, `${accountPath(account.id)}/folders`),
|
|
11960
|
+
messages: (account, folderId, cursor) => mailCall(
|
|
11961
|
+
arbi,
|
|
11962
|
+
`${accountPath(account.id)}/messages?${new URLSearchParams({
|
|
11963
|
+
folder_id: folderId,
|
|
11964
|
+
q: "",
|
|
11965
|
+
limit: "100",
|
|
11966
|
+
...cursor ? { cursor } : {}
|
|
11967
|
+
})}`
|
|
11968
|
+
),
|
|
11969
|
+
message: async (account, messageId) => {
|
|
11970
|
+
const path = `${accountPath(account.id)}/messages/${encodeURIComponent(messageId)}`;
|
|
11971
|
+
try {
|
|
11972
|
+
return await mailCall(arbi, path);
|
|
11973
|
+
} catch (error) {
|
|
11974
|
+
if (error.status !== 401 || account.provider !== "native")
|
|
11975
|
+
throw error;
|
|
11976
|
+
if (hasLiveSession(arbi)) await arbi.mail.unlockNative();
|
|
11977
|
+
return mailCall(arbi, path);
|
|
11978
|
+
}
|
|
11979
|
+
},
|
|
11980
|
+
attachment: async (account, messageId, attachmentId) => {
|
|
11981
|
+
const response = await arbi.mail.fetch(
|
|
11982
|
+
`${accountPath(account.id)}/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(attachmentId)}`,
|
|
11983
|
+
{},
|
|
11984
|
+
unlocked
|
|
11985
|
+
);
|
|
11986
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
11987
|
+
}
|
|
11988
|
+
};
|
|
11989
|
+
}
|
|
11990
|
+
function liveSender(arbi) {
|
|
11991
|
+
const transport = liveTransport(arbi);
|
|
11992
|
+
return {
|
|
11993
|
+
prepareSecure: async (item) => {
|
|
11994
|
+
if (item.account.provider !== "native")
|
|
11995
|
+
throw new Error("Choose your native inbox to send securely");
|
|
11996
|
+
return arbi.secureMail.prepare(item.compose, item.account, item.requestId);
|
|
11997
|
+
},
|
|
11998
|
+
unlock: async (account) => {
|
|
11999
|
+
if (account.provider !== "native") await transport.unlock(account);
|
|
12000
|
+
},
|
|
12001
|
+
send: (item) => item.secure ? mailJson((p, i) => arbi.mail.fetch(p, i), "/secure/send", {
|
|
12002
|
+
method: "POST",
|
|
12003
|
+
body: JSON.stringify(item.securePayload)
|
|
12004
|
+
}) : mailJson(
|
|
12005
|
+
(p, i) => arbi.mail.fetch(p, i),
|
|
12006
|
+
`${accountPath(item.account.id)}/send`,
|
|
12007
|
+
{
|
|
12008
|
+
method: "POST",
|
|
12009
|
+
body: JSON.stringify({ ...item.compose, request_id: item.requestId })
|
|
12010
|
+
}
|
|
12011
|
+
)
|
|
12012
|
+
};
|
|
12013
|
+
}
|
|
12014
|
+
var localLocks = /* @__PURE__ */ new Set();
|
|
12015
|
+
async function exclusive(name, fn) {
|
|
12016
|
+
if (typeof navigator !== "undefined" && navigator.locks?.request) {
|
|
12017
|
+
return navigator.locks.request(name, { ifAvailable: true }, async (lock) => {
|
|
12018
|
+
if (!lock) return false;
|
|
12019
|
+
await fn();
|
|
12020
|
+
return true;
|
|
12021
|
+
});
|
|
12022
|
+
}
|
|
12023
|
+
if (localLocks.has(name)) return false;
|
|
12024
|
+
localLocks.add(name);
|
|
12025
|
+
try {
|
|
12026
|
+
await fn();
|
|
12027
|
+
return true;
|
|
12028
|
+
} finally {
|
|
12029
|
+
localLocks.delete(name);
|
|
12030
|
+
}
|
|
12031
|
+
}
|
|
12032
|
+
var retryTimer = null;
|
|
12033
|
+
async function flushOutbox(context) {
|
|
12034
|
+
const { arbi, queryClient, openOutbox } = context;
|
|
12035
|
+
if (!canReachMail(arbi)) return;
|
|
12036
|
+
const ran = await exclusive("arbi-mail-outbox", async () => {
|
|
12037
|
+
const result = await flushOutboxOnce(liveSender(arbi), () => onlineManager.isOnline());
|
|
12038
|
+
if (result.sent > 0) void queryClient.invalidateQueries({ queryKey: mailQueryKeys.all() });
|
|
12039
|
+
const remaining = (await listOutbox()).filter((i) => i.status !== "sent");
|
|
12040
|
+
if (result.failed > 0) {
|
|
12041
|
+
notify.error(
|
|
12042
|
+
result.failed === 1 ? "A message in your Outbox couldn't be sent" : `${result.failed} messages in your Outbox couldn't be sent`,
|
|
12043
|
+
{
|
|
12044
|
+
description: "Open the Outbox to see why, then retry, edit or delete it.",
|
|
12045
|
+
...openOutbox && { action: { label: "Open Outbox", onClick: openOutbox } }
|
|
12046
|
+
}
|
|
12047
|
+
);
|
|
12048
|
+
} else if (result.sent > 0 && remaining.length === 0) {
|
|
12049
|
+
notify.success(
|
|
12050
|
+
result.sent === 1 ? "Your Outbox message was sent" : `${result.sent} Outbox messages sent`
|
|
12051
|
+
);
|
|
12052
|
+
}
|
|
12053
|
+
setTimeout(() => void purgeSentOutbox(3e4), 31e3);
|
|
12054
|
+
});
|
|
12055
|
+
if (!ran) {
|
|
12056
|
+
if (retryTimer) clearTimeout(retryTimer);
|
|
12057
|
+
retryTimer = setTimeout(() => void flushOutbox(context), 3e3);
|
|
12058
|
+
}
|
|
12059
|
+
}
|
|
12060
|
+
async function uploadOfflineDrafts({
|
|
12061
|
+
arbi,
|
|
12062
|
+
queryClient
|
|
12063
|
+
}) {
|
|
12064
|
+
if (!canReachMail(arbi)) return;
|
|
12065
|
+
await exclusive("arbi-mail-drafts", async () => {
|
|
12066
|
+
const uploaded = await syncLocalDrafts(
|
|
12067
|
+
({ accountId, compose }) => mailJson(
|
|
12068
|
+
(p, i) => arbi.mail.fetch(p, i),
|
|
12069
|
+
`${accountPath(accountId)}/drafts`,
|
|
12070
|
+
{
|
|
12071
|
+
method: "POST",
|
|
12072
|
+
body: JSON.stringify(compose)
|
|
12073
|
+
}
|
|
12074
|
+
)
|
|
12075
|
+
);
|
|
12076
|
+
if (uploaded > 0) {
|
|
12077
|
+
void queryClient.invalidateQueries({ queryKey: mailQueryKeys.allMessages() });
|
|
12078
|
+
notify.success(
|
|
12079
|
+
uploaded === 1 ? "Your offline draft was saved to Drafts" : `${uploaded} offline drafts were saved to Drafts`
|
|
12080
|
+
);
|
|
12081
|
+
}
|
|
12082
|
+
});
|
|
12083
|
+
}
|
|
12084
|
+
var syncing = false;
|
|
12085
|
+
function isMailSyncRunning() {
|
|
12086
|
+
return syncing;
|
|
12087
|
+
}
|
|
12088
|
+
async function syncOfflineMailbox({ arbi }, options = {}) {
|
|
12089
|
+
const settings = getOfflineMailSettings(sessionUser(arbi));
|
|
12090
|
+
if (!settings.enabled || !canReachMail(arbi)) return;
|
|
12091
|
+
await exclusive("arbi-mail-sync", async () => {
|
|
12092
|
+
syncing = true;
|
|
12093
|
+
notifyOfflineMailChange("status");
|
|
12094
|
+
try {
|
|
12095
|
+
await syncMailbox(liveTransport(arbi), settings, options);
|
|
12096
|
+
} catch (error) {
|
|
12097
|
+
logger.error("[offline-mail] sync failed", error);
|
|
12098
|
+
} finally {
|
|
12099
|
+
syncing = false;
|
|
12100
|
+
notifyOfflineMailChange("status");
|
|
12101
|
+
}
|
|
12102
|
+
});
|
|
12103
|
+
}
|
|
12104
|
+
|
|
12105
|
+
// src/mail/offline/reads.ts
|
|
12106
|
+
var NOT_SAVED_HINT = 'Turn on "Keep my mailbox available offline" in Mail settings to read mail without a connection.';
|
|
12107
|
+
async function onlineFirst(online, offline, unavailable) {
|
|
12108
|
+
if (onlineManager.isOnline()) {
|
|
12109
|
+
try {
|
|
12110
|
+
return await online();
|
|
12111
|
+
} catch (error) {
|
|
12112
|
+
if (!isNetworkFailure(error)) throw error;
|
|
12113
|
+
}
|
|
12114
|
+
}
|
|
12115
|
+
const saved = await offline().catch(() => void 0);
|
|
12116
|
+
if (saved !== void 0) return saved;
|
|
12117
|
+
throw new MailOfflineUnavailableError(unavailable);
|
|
12118
|
+
}
|
|
12119
|
+
function offlineMailReads(getArbi, { keepsMail = () => true } = {}) {
|
|
12120
|
+
const call = (path) => mailJson((p, i) => getArbi().mail.fetch(p, i), path);
|
|
12121
|
+
const storeEnabled = () => getOfflineMailSettings(sessionUser(getArbi())).enabled;
|
|
12122
|
+
const remember = (write2) => {
|
|
12123
|
+
if (!keepsMail()) return;
|
|
12124
|
+
write2().catch((error) => logger.warn("[offline-mail] could not save for offline use", error));
|
|
12125
|
+
};
|
|
12126
|
+
return {
|
|
12127
|
+
getPolicy: () => onlineFirst(
|
|
12128
|
+
async () => {
|
|
12129
|
+
const policy = await call("/policy");
|
|
12130
|
+
remember(() => savePolicy(policy));
|
|
12131
|
+
return policy;
|
|
12132
|
+
},
|
|
12133
|
+
loadPolicy,
|
|
12134
|
+
"You're offline. Mail opens here once you've used it online on this device."
|
|
12135
|
+
),
|
|
12136
|
+
listAccounts: () => onlineFirst(
|
|
12137
|
+
async () => {
|
|
12138
|
+
const accounts = await call("/accounts");
|
|
12139
|
+
remember(() => saveAccounts(accounts));
|
|
12140
|
+
return accounts;
|
|
12141
|
+
},
|
|
12142
|
+
loadAccounts,
|
|
12143
|
+
"You're offline. Your mail accounts appear here once you've opened Mail online on this device."
|
|
12144
|
+
),
|
|
12145
|
+
listFolders: (account) => onlineFirst(
|
|
12146
|
+
async () => {
|
|
12147
|
+
const folders = await call(`${accountPath(account.id)}/folders`);
|
|
12148
|
+
if (storeEnabled()) remember(() => saveFolders(account.id, folders));
|
|
12149
|
+
return folders;
|
|
12150
|
+
},
|
|
12151
|
+
() => loadFolders(account.id),
|
|
12152
|
+
`You're offline and this mailbox isn't saved on this device. ${NOT_SAVED_HINT}`
|
|
12153
|
+
),
|
|
12154
|
+
listMessages: (account, folderId, query, cursor) => onlineFirst(
|
|
12155
|
+
async () => {
|
|
12156
|
+
const page = await call(
|
|
12157
|
+
`${accountPath(account.id)}/messages?${new URLSearchParams({ folder_id: folderId, q: query, ...cursor ? { cursor } : {} })}`
|
|
12158
|
+
);
|
|
12159
|
+
if (storeEnabled() && !query)
|
|
12160
|
+
remember(async () => {
|
|
12161
|
+
for (const message of page.messages) await saveHeader(account.id, folderId, message);
|
|
12162
|
+
});
|
|
12163
|
+
return page;
|
|
12164
|
+
},
|
|
12165
|
+
async () => {
|
|
12166
|
+
if (!await hasFolder(account.id)) return void 0;
|
|
12167
|
+
return { messages: await searchFolder(account.id, folderId, query), fromDevice: true };
|
|
12168
|
+
},
|
|
12169
|
+
`You're offline and this folder isn't saved on this device. ${NOT_SAVED_HINT}`
|
|
12170
|
+
),
|
|
12171
|
+
getMessage: (account, messageId) => onlineFirst(
|
|
12172
|
+
async () => {
|
|
12173
|
+
const message = await call(
|
|
12174
|
+
`${accountPath(account.id)}/messages/${encodeURIComponent(messageId)}`
|
|
12175
|
+
);
|
|
12176
|
+
if (storeEnabled()) remember(() => saveBody(account.id, message));
|
|
12177
|
+
return message;
|
|
12178
|
+
},
|
|
12179
|
+
() => loadBody(account.id, messageId),
|
|
12180
|
+
storeEnabled() ? "You're offline and this message hasn't been saved on this device yet." : `You're offline. ${NOT_SAVED_HINT}`
|
|
12181
|
+
)
|
|
12182
|
+
};
|
|
12183
|
+
}
|
|
12184
|
+
|
|
12185
|
+
// src/mail/offline/wipe.ts
|
|
12186
|
+
var OWNER_KEY2 = "owner";
|
|
12187
|
+
async function wipeOfflineMail() {
|
|
12188
|
+
forgetOfflineMailKeys();
|
|
12189
|
+
await deleteOfflineMailDatabase();
|
|
12190
|
+
notifyOfflineMailChange("wiped");
|
|
12191
|
+
}
|
|
12192
|
+
async function claimOfflineMail(userExtId) {
|
|
12193
|
+
const owner = await getMeta(OWNER_KEY2);
|
|
12194
|
+
if (owner && owner !== userExtId) await wipeOfflineMail();
|
|
12195
|
+
await setMeta(OWNER_KEY2, userExtId);
|
|
12196
|
+
}
|
|
12197
|
+
|
|
12198
|
+
// src/mail/offline/runtime.ts
|
|
12199
|
+
var SYNC_INTERVAL_MS = 5 * 60 * 1e3;
|
|
12200
|
+
var NEW_MAIL_DEBOUNCE_MS = 5e3;
|
|
12201
|
+
function useOfflineMailRuntime({ openOutbox } = {}) {
|
|
12202
|
+
const { user, status, serverSessionKey } = useArbiSession();
|
|
12203
|
+
const userExtId = user?.external_id;
|
|
12204
|
+
const authenticated = status === "signed-in";
|
|
12205
|
+
const mailEnabled = useFeature("mail");
|
|
12206
|
+
const entitled = useIsEntitled("mail");
|
|
12207
|
+
const hasSession = !!serverSessionKey;
|
|
12208
|
+
const active = authenticated && mailEnabled && entitled && !!userExtId;
|
|
12209
|
+
const queryClient = useQueryClient();
|
|
12210
|
+
const arbi = useArbi();
|
|
12211
|
+
const latest = useRef({ arbi, queryClient, openOutbox });
|
|
12212
|
+
latest.current = { arbi, queryClient, openOutbox };
|
|
12213
|
+
useEffect(() => {
|
|
12214
|
+
if (!active || !userExtId) return;
|
|
12215
|
+
claimOfflineMail(userExtId).catch(
|
|
12216
|
+
(error) => logger.warn("[offline-mail] could not bind the store", error)
|
|
12217
|
+
);
|
|
12218
|
+
}, [active, userExtId]);
|
|
12219
|
+
useEffect(() => {
|
|
12220
|
+
if (!active || !hasSession) return;
|
|
12221
|
+
const context = () => latest.current;
|
|
12222
|
+
const catchUp = async () => {
|
|
12223
|
+
await flushOutbox(context());
|
|
12224
|
+
await uploadOfflineDrafts(context());
|
|
12225
|
+
await syncOfflineMailbox(context());
|
|
12226
|
+
};
|
|
12227
|
+
const run2 = () => {
|
|
12228
|
+
catchUp().catch((error) => logger.warn("[offline-mail] catch-up failed", error));
|
|
12229
|
+
};
|
|
12230
|
+
run2();
|
|
12231
|
+
let wasOnline = onlineManager.isOnline();
|
|
12232
|
+
const offOnline = onlineManager.subscribe((online) => {
|
|
12233
|
+
if (online && !wasOnline) run2();
|
|
12234
|
+
wasOnline = online;
|
|
12235
|
+
});
|
|
12236
|
+
const interval = setInterval(() => {
|
|
12237
|
+
if (document.visibilityState === "visible") void syncOfflineMailbox(context());
|
|
12238
|
+
}, SYNC_INTERVAL_MS);
|
|
12239
|
+
const enabled = () => getOfflineMailSettings(sessionUser(context().arbi)).enabled;
|
|
12240
|
+
let newMailTimer = null;
|
|
12241
|
+
const offCache = queryClient.getQueryCache().subscribe((event) => {
|
|
12242
|
+
if (event.type !== "updated" || event.action.type !== "invalidate") return;
|
|
12243
|
+
if (!isUnderQueryKey(event.query.queryKey, mailQueryKeys.all()) || !enabled()) return;
|
|
12244
|
+
if (newMailTimer) clearTimeout(newMailTimer);
|
|
12245
|
+
newMailTimer = setTimeout(() => void syncOfflineMailbox(context()), NEW_MAIL_DEBOUNCE_MS);
|
|
12246
|
+
});
|
|
12247
|
+
const offSettings = onOfflineMailChange((change) => {
|
|
12248
|
+
if (change === "settings" && enabled()) void syncOfflineMailbox(context(), { full: true });
|
|
12249
|
+
if (change === "outbox") void flushOutbox(context());
|
|
12250
|
+
if (change === "drafts") void uploadOfflineDrafts(context());
|
|
12251
|
+
});
|
|
12252
|
+
return () => {
|
|
12253
|
+
offOnline();
|
|
12254
|
+
clearInterval(interval);
|
|
12255
|
+
if (newMailTimer) clearTimeout(newMailTimer);
|
|
12256
|
+
offCache();
|
|
12257
|
+
offSettings();
|
|
12258
|
+
};
|
|
12259
|
+
}, [active, hasSession, queryClient]);
|
|
12260
|
+
}
|
|
12261
|
+
function useStoreValue(load, initial, changes) {
|
|
12262
|
+
const [value, setValue] = useState(initial);
|
|
12263
|
+
const key = changes.join(",");
|
|
12264
|
+
useEffect(() => {
|
|
12265
|
+
let alive = true;
|
|
12266
|
+
const read = () => load().then((next) => alive && setValue(next)).catch((error) => logger.warn("[offline-mail] read failed", error));
|
|
12267
|
+
void read();
|
|
12268
|
+
const off = onOfflineMailChange((change) => {
|
|
12269
|
+
if (change === "wiped") setValue(initial);
|
|
12270
|
+
if (change === "wiped" || key.split(",").includes(change)) void read();
|
|
12271
|
+
});
|
|
12272
|
+
return () => {
|
|
12273
|
+
alive = false;
|
|
12274
|
+
off();
|
|
12275
|
+
};
|
|
12276
|
+
}, [key, load, initial]);
|
|
12277
|
+
return value;
|
|
12278
|
+
}
|
|
12279
|
+
function useOfflineMailSettings() {
|
|
12280
|
+
const userExtId = useArbiSession().user?.external_id;
|
|
12281
|
+
const subscribe = useCallback(
|
|
12282
|
+
(onChange) => onOfflineMailChange((change) => {
|
|
12283
|
+
if (change === "settings") onChange();
|
|
12284
|
+
}),
|
|
12285
|
+
[]
|
|
12286
|
+
);
|
|
12287
|
+
const raw = useSyncExternalStore(
|
|
12288
|
+
subscribe,
|
|
12289
|
+
() => JSON.stringify([userExtId, getOfflineMailSettings(userExtId)])
|
|
12290
|
+
);
|
|
12291
|
+
const settings = useMemo(() => JSON.parse(raw)[1], [raw]);
|
|
12292
|
+
const update2 = useCallback(
|
|
12293
|
+
(patch) => void setOfflineMailSettings(userExtId, patch),
|
|
12294
|
+
[userExtId]
|
|
12295
|
+
);
|
|
12296
|
+
return [settings, update2];
|
|
12297
|
+
}
|
|
12298
|
+
var EMPTY_STATUS = { messages: 0, bytes: 0, skipped: [] };
|
|
12299
|
+
function useOfflineMailStatus() {
|
|
12300
|
+
const status = useStoreValue(getOfflineMailStatus, EMPTY_STATUS, ["status", "mailbox"]);
|
|
12301
|
+
const [syncing2, setSyncing] = useState(isMailSyncRunning);
|
|
12302
|
+
useEffect(
|
|
12303
|
+
() => onOfflineMailChange((change) => {
|
|
12304
|
+
if (change === "status") setSyncing(isMailSyncRunning());
|
|
12305
|
+
}),
|
|
12306
|
+
[]
|
|
12307
|
+
);
|
|
12308
|
+
return { ...status, syncing: syncing2 };
|
|
12309
|
+
}
|
|
12310
|
+
var NO_ITEMS = [];
|
|
12311
|
+
function useOutbox(enabled = true) {
|
|
12312
|
+
const load = useCallback(() => enabled ? listOutbox() : Promise.resolve(NO_ITEMS), [enabled]);
|
|
12313
|
+
return useStoreValue(load, NO_ITEMS, ["outbox"]);
|
|
12314
|
+
}
|
|
12315
|
+
var NO_DRAFTS = [];
|
|
12316
|
+
function useLocalDrafts(accountId) {
|
|
12317
|
+
const load = useCallback(
|
|
12318
|
+
() => accountId ? listLocalDrafts(accountId) : Promise.resolve(NO_DRAFTS),
|
|
12319
|
+
[accountId]
|
|
12320
|
+
);
|
|
12321
|
+
return useStoreValue(load, NO_DRAFTS, ["drafts"]);
|
|
12322
|
+
}
|
|
12323
|
+
function useFlushOutbox() {
|
|
12324
|
+
const arbi = useArbi();
|
|
12325
|
+
const queryClient = useQueryClient();
|
|
12326
|
+
return useCallback(() => flushOutbox({ arbi, queryClient }), [arbi, queryClient]);
|
|
12327
|
+
}
|
|
12328
|
+
function useSyncOfflineMailbox() {
|
|
12329
|
+
const arbi = useArbi();
|
|
12330
|
+
return useCallback((options) => syncOfflineMailbox({ arbi }, options), [arbi]);
|
|
12331
|
+
}
|
|
12332
|
+
|
|
12333
|
+
// src/mail/offline/confirm-sign-out.ts
|
|
12334
|
+
async function confirmLosingOfflineMail(confirm) {
|
|
12335
|
+
const [outbox, drafts] = await Promise.all([
|
|
12336
|
+
listOutbox().catch(() => []),
|
|
12337
|
+
listLocalDrafts().catch(() => [])
|
|
12338
|
+
]);
|
|
12339
|
+
const unsent = outbox.filter((item) => item.status !== "sent").length;
|
|
12340
|
+
if (unsent === 0 && drafts.length === 0) return true;
|
|
12341
|
+
const parts = [
|
|
12342
|
+
unsent > 0 ? `${unsent} unsent ${unsent === 1 ? "message" : "messages"} in your Outbox` : "",
|
|
12343
|
+
drafts.length > 0 ? `${drafts.length} ${drafts.length === 1 ? "draft" : "drafts"} saved only on this device` : ""
|
|
12344
|
+
].filter(Boolean);
|
|
12345
|
+
return confirm({
|
|
12346
|
+
message: `You have ${parts.join(" and ")}. Signing out deletes ${unsent + drafts.length === 1 ? "it" : "them"} from this device. Reconnect first to send and save them.`,
|
|
12347
|
+
title: "Sign out and delete unsent mail?"
|
|
12348
|
+
});
|
|
12349
|
+
}
|
|
11127
12350
|
function useConnectorMutation(mutationFn) {
|
|
11128
12351
|
const qc = useQueryClient();
|
|
11129
12352
|
return useMutation({
|
|
@@ -11591,9 +12814,9 @@ function useDeleteCalendar() {
|
|
|
11591
12814
|
function useCreateEvent() {
|
|
11592
12815
|
const arbi = useArbi();
|
|
11593
12816
|
const qc = useQueryClient();
|
|
11594
|
-
const { write, networkMode } = useOfflineWrite();
|
|
12817
|
+
const { write: write2, networkMode } = useOfflineWrite();
|
|
11595
12818
|
return useMutation({
|
|
11596
|
-
mutationFn: (body) =>
|
|
12819
|
+
mutationFn: (body) => write2({ kind: "create", body }, () => arbi.calendar.createEvent(body)),
|
|
11597
12820
|
networkMode,
|
|
11598
12821
|
onSuccess: () => {
|
|
11599
12822
|
void invalidateCalendarEvents(qc);
|
|
@@ -11603,9 +12826,9 @@ function useCreateEvent() {
|
|
|
11603
12826
|
function useUpdateEvent() {
|
|
11604
12827
|
const arbi = useArbi();
|
|
11605
12828
|
const qc = useQueryClient();
|
|
11606
|
-
const { write, networkMode } = useOfflineWrite();
|
|
12829
|
+
const { write: write2, networkMode } = useOfflineWrite();
|
|
11607
12830
|
return useMutation({
|
|
11608
|
-
mutationFn: ({ target, body }) =>
|
|
12831
|
+
mutationFn: ({ target, body }) => write2({ kind: "update", target, body }, () => arbi.calendar.updateEvent(target, body)),
|
|
11609
12832
|
networkMode,
|
|
11610
12833
|
onSuccess: () => {
|
|
11611
12834
|
void invalidateCalendarEvents(qc);
|
|
@@ -11615,9 +12838,9 @@ function useUpdateEvent() {
|
|
|
11615
12838
|
function useDeleteEvent() {
|
|
11616
12839
|
const arbi = useArbi();
|
|
11617
12840
|
const qc = useQueryClient();
|
|
11618
|
-
const { write, networkMode } = useOfflineWrite();
|
|
12841
|
+
const { write: write2, networkMode } = useOfflineWrite();
|
|
11619
12842
|
return useMutation({
|
|
11620
|
-
mutationFn: (target) =>
|
|
12843
|
+
mutationFn: (target) => write2({ kind: "delete", target }, () => arbi.calendar.deleteEvent(target)),
|
|
11621
12844
|
networkMode,
|
|
11622
12845
|
onSuccess: () => {
|
|
11623
12846
|
void invalidateCalendarEvents(qc);
|
|
@@ -11637,9 +12860,9 @@ function useMoveEvent() {
|
|
|
11637
12860
|
function useRespondToEvent() {
|
|
11638
12861
|
const arbi = useArbi();
|
|
11639
12862
|
const qc = useQueryClient();
|
|
11640
|
-
const { write, networkMode } = useOfflineWrite();
|
|
12863
|
+
const { write: write2, networkMode } = useOfflineWrite();
|
|
11641
12864
|
return useMutation({
|
|
11642
|
-
mutationFn: (params) =>
|
|
12865
|
+
mutationFn: (params) => write2({ kind: "respond", params }, () => arbi.calendar.respond(params)),
|
|
11643
12866
|
networkMode,
|
|
11644
12867
|
onSuccess: () => {
|
|
11645
12868
|
void invalidateCalendarEvents(qc);
|
|
@@ -11879,7 +13102,7 @@ function useSyncAccountsOnOpen(enabled = true) {
|
|
|
11879
13102
|
const arbi = useArbi();
|
|
11880
13103
|
const { data: accounts } = useCalendarAccounts(enabled);
|
|
11881
13104
|
const qc = useQueryClient();
|
|
11882
|
-
const [
|
|
13105
|
+
const [syncing2, setSyncing] = useState(false);
|
|
11883
13106
|
useEffect(() => {
|
|
11884
13107
|
if (!enabled || !accounts?.length) return;
|
|
11885
13108
|
const due = accounts.filter((a) => !syncedThisSession.has(a.id));
|
|
@@ -11891,7 +13114,7 @@ function useSyncAccountsOnOpen(enabled = true) {
|
|
|
11891
13114
|
void invalidateAccounts(qc);
|
|
11892
13115
|
});
|
|
11893
13116
|
}, [enabled, accounts, arbi, qc]);
|
|
11894
|
-
return { syncing };
|
|
13117
|
+
return { syncing: syncing2 };
|
|
11895
13118
|
}
|
|
11896
13119
|
function useBookingPages(enabled = true) {
|
|
11897
13120
|
const arbi = useArbi();
|
|
@@ -11919,6 +13142,349 @@ function useDeleteBookingPage() {
|
|
|
11919
13142
|
onSuccess: () => qc.invalidateQueries({ queryKey: calendarQueryKeys.bookingPages() })
|
|
11920
13143
|
});
|
|
11921
13144
|
}
|
|
13145
|
+
|
|
13146
|
+
// src/offline-store/wipe.ts
|
|
13147
|
+
async function wipeStore(sealer3, store3) {
|
|
13148
|
+
sealer3.forget();
|
|
13149
|
+
await store3.deleteDatabase();
|
|
13150
|
+
}
|
|
13151
|
+
|
|
13152
|
+
// src/calendar/offline/outbox.ts
|
|
13153
|
+
var OFFLINE_CALENDAR_DB = "arbi-offline-calendar";
|
|
13154
|
+
var CACHE_LIMIT = 200;
|
|
13155
|
+
var OfflineCalendarLockedError = class extends Error {
|
|
13156
|
+
constructor() {
|
|
13157
|
+
super("Sign in to open the calendar saved on this device");
|
|
13158
|
+
this.name = "OfflineCalendarLockedError";
|
|
13159
|
+
}
|
|
13160
|
+
};
|
|
13161
|
+
var store2 = createRecordStore(OFFLINE_CALENDAR_DB);
|
|
13162
|
+
var sealer2 = createOfflineSealer("arbi-offline-calendar-v1", {
|
|
13163
|
+
locked: () => new OfflineCalendarLockedError()
|
|
13164
|
+
});
|
|
13165
|
+
function isCalendarOffline() {
|
|
13166
|
+
return isBrowserOffline();
|
|
13167
|
+
}
|
|
13168
|
+
function isCalendarNetworkError(error) {
|
|
13169
|
+
if (isNetworkFailure(error)) return true;
|
|
13170
|
+
const status = error?.status;
|
|
13171
|
+
return typeof status !== "number" && isBrowserOffline();
|
|
13172
|
+
}
|
|
13173
|
+
var cacheBucket = () => sealer2.blind("bucket", "cache");
|
|
13174
|
+
var cacheKey = (key) => sealer2.blind("cache", key);
|
|
13175
|
+
async function writeCalendarCache(key, data) {
|
|
13176
|
+
const sealed = await sealer2.sealJson({ key, data });
|
|
13177
|
+
const now = Date.now();
|
|
13178
|
+
await store2.putRecord({
|
|
13179
|
+
key: await cacheKey(key),
|
|
13180
|
+
kind: "cache",
|
|
13181
|
+
bucket: await cacheBucket(),
|
|
13182
|
+
sortAt: now,
|
|
13183
|
+
size: sealed.length,
|
|
13184
|
+
lastAccess: now,
|
|
13185
|
+
data: sealed
|
|
13186
|
+
});
|
|
13187
|
+
await evictCache();
|
|
13188
|
+
}
|
|
13189
|
+
async function readCalendarCache(key) {
|
|
13190
|
+
try {
|
|
13191
|
+
const blindKey = await cacheKey(key);
|
|
13192
|
+
const record = await store2.getRecord(blindKey);
|
|
13193
|
+
if (!record) return null;
|
|
13194
|
+
const opened = await sealer2.openJson(record.data);
|
|
13195
|
+
if (opened.key !== key) return null;
|
|
13196
|
+
await store2.putRecord({ ...record, lastAccess: Date.now() });
|
|
13197
|
+
return opened.data;
|
|
13198
|
+
} catch {
|
|
13199
|
+
return null;
|
|
13200
|
+
}
|
|
13201
|
+
}
|
|
13202
|
+
async function evictCache() {
|
|
13203
|
+
const rows = (await store2.allRecordAccess()).filter((r) => r.kind === "cache");
|
|
13204
|
+
if (rows.length <= CACHE_LIMIT) return;
|
|
13205
|
+
rows.sort((a, b) => a.lastAccess - b.lastAccess);
|
|
13206
|
+
await store2.deleteRecords(rows.slice(0, rows.length - CACHE_LIMIT).map((r) => r.key));
|
|
13207
|
+
}
|
|
13208
|
+
var outboxBucket = () => sealer2.blind("bucket", "outbox");
|
|
13209
|
+
var outboxKey = (requestId) => sealer2.blind("outbox", requestId);
|
|
13210
|
+
var seqCounter2 = 0;
|
|
13211
|
+
async function writeItem(item) {
|
|
13212
|
+
const sealed = await sealer2.sealJson(item);
|
|
13213
|
+
await store2.putRecord({
|
|
13214
|
+
key: await outboxKey(item.requestId),
|
|
13215
|
+
kind: "outbox",
|
|
13216
|
+
bucket: await outboxBucket(),
|
|
13217
|
+
sortAt: item.seq,
|
|
13218
|
+
size: sealed.length,
|
|
13219
|
+
lastAccess: Date.now(),
|
|
13220
|
+
data: sealed
|
|
13221
|
+
});
|
|
13222
|
+
}
|
|
13223
|
+
async function readItems() {
|
|
13224
|
+
const records = await store2.recordsInBucket(await outboxBucket());
|
|
13225
|
+
const items = [];
|
|
13226
|
+
for (const record of records) {
|
|
13227
|
+
try {
|
|
13228
|
+
items.push(await sealer2.openJson(record.data));
|
|
13229
|
+
} catch {
|
|
13230
|
+
}
|
|
13231
|
+
}
|
|
13232
|
+
return items.sort((a, b) => a.seq - b.seq);
|
|
13233
|
+
}
|
|
13234
|
+
function newRequestId() {
|
|
13235
|
+
return typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
13236
|
+
}
|
|
13237
|
+
async function enqueueCalendarWrite(op) {
|
|
13238
|
+
const now = Date.now();
|
|
13239
|
+
seqCounter2 = Math.max(seqCounter2 + 1, now * 1e3);
|
|
13240
|
+
const item = {
|
|
13241
|
+
requestId: newRequestId(),
|
|
13242
|
+
op,
|
|
13243
|
+
state: "queued",
|
|
13244
|
+
attempts: 0,
|
|
13245
|
+
createdAt: now,
|
|
13246
|
+
seq: seqCounter2
|
|
13247
|
+
};
|
|
13248
|
+
await writeItem(item);
|
|
13249
|
+
notify2("enqueued");
|
|
13250
|
+
return { requestId: item.requestId };
|
|
13251
|
+
}
|
|
13252
|
+
async function listCalendarOutbox() {
|
|
13253
|
+
try {
|
|
13254
|
+
return (await readItems()).map(({ requestId, op, state, inDoubt, error }) => ({
|
|
13255
|
+
requestId,
|
|
13256
|
+
op,
|
|
13257
|
+
state,
|
|
13258
|
+
inDoubt,
|
|
13259
|
+
error
|
|
13260
|
+
}));
|
|
13261
|
+
} catch {
|
|
13262
|
+
return [];
|
|
13263
|
+
}
|
|
13264
|
+
}
|
|
13265
|
+
function classify2(error) {
|
|
13266
|
+
if (isCalendarNetworkError(error)) return "offline";
|
|
13267
|
+
const status = error?.status;
|
|
13268
|
+
if (typeof status === "number" && status >= 500) return "retry";
|
|
13269
|
+
return "failed";
|
|
13270
|
+
}
|
|
13271
|
+
function run(exec, item) {
|
|
13272
|
+
const op = item.op;
|
|
13273
|
+
switch (op.kind) {
|
|
13274
|
+
case "create":
|
|
13275
|
+
return exec.create(op.body, item.requestId);
|
|
13276
|
+
case "update":
|
|
13277
|
+
return exec.update(op.target, op.body, item.requestId);
|
|
13278
|
+
case "delete":
|
|
13279
|
+
return exec.delete(op.target, item.requestId);
|
|
13280
|
+
case "respond":
|
|
13281
|
+
return exec.respond(op.params, item.requestId);
|
|
13282
|
+
}
|
|
13283
|
+
}
|
|
13284
|
+
var replaying = null;
|
|
13285
|
+
function replayCalendarOutbox(exec) {
|
|
13286
|
+
if (replaying) return replaying;
|
|
13287
|
+
const locks = typeof navigator !== "undefined" ? navigator.locks : void 0;
|
|
13288
|
+
const run2 = locks?.request ? locks.request(
|
|
13289
|
+
"arbi-calendar-outbox-replay",
|
|
13290
|
+
() => replayOnce(exec)
|
|
13291
|
+
) : replayOnce(exec);
|
|
13292
|
+
const current = run2.finally(() => {
|
|
13293
|
+
replaying = null;
|
|
13294
|
+
});
|
|
13295
|
+
replaying = current;
|
|
13296
|
+
return current;
|
|
13297
|
+
}
|
|
13298
|
+
async function replayOnce(exec) {
|
|
13299
|
+
const result = { sent: 0, failed: 0, inDoubt: 0 };
|
|
13300
|
+
for (const queued of await readItems()) {
|
|
13301
|
+
if (queued.state !== "queued" && queued.state !== "sending") continue;
|
|
13302
|
+
if (isCalendarOffline()) break;
|
|
13303
|
+
let item = {
|
|
13304
|
+
...queued,
|
|
13305
|
+
inDoubt: queued.inDoubt || queued.state === "sending"
|
|
13306
|
+
};
|
|
13307
|
+
if (item.op.kind === "create" && item.inDoubt && exec.findCreated) {
|
|
13308
|
+
let found = false;
|
|
13309
|
+
try {
|
|
13310
|
+
found = await exec.findCreated(item.requestId, item.op.body);
|
|
13311
|
+
} catch (error) {
|
|
13312
|
+
if (classify2(error) === "offline") break;
|
|
13313
|
+
}
|
|
13314
|
+
if (found) {
|
|
13315
|
+
await store2.deleteRecords([await outboxKey(item.requestId)]);
|
|
13316
|
+
result.sent++;
|
|
13317
|
+
continue;
|
|
13318
|
+
}
|
|
13319
|
+
}
|
|
13320
|
+
item = { ...item, state: "sending", attempts: item.attempts + 1 };
|
|
13321
|
+
await writeItem(item);
|
|
13322
|
+
let outcome;
|
|
13323
|
+
let message;
|
|
13324
|
+
try {
|
|
13325
|
+
await run(exec, item);
|
|
13326
|
+
outcome = "sent";
|
|
13327
|
+
} catch (error) {
|
|
13328
|
+
outcome = classify2(error);
|
|
13329
|
+
message = error instanceof Error ? error.message : "The change could not be saved";
|
|
13330
|
+
}
|
|
13331
|
+
if (outcome === "sent") {
|
|
13332
|
+
await store2.deleteRecords([await outboxKey(item.requestId)]);
|
|
13333
|
+
result.sent++;
|
|
13334
|
+
continue;
|
|
13335
|
+
}
|
|
13336
|
+
if (outcome === "failed") {
|
|
13337
|
+
await writeItem({ ...item, state: "failed", error: message });
|
|
13338
|
+
result.failed++;
|
|
13339
|
+
continue;
|
|
13340
|
+
}
|
|
13341
|
+
const inDoubt = item.inDoubt || item.op.kind === "create";
|
|
13342
|
+
await writeItem({ ...item, state: "queued", inDoubt, error: message });
|
|
13343
|
+
if (inDoubt) result.inDoubt++;
|
|
13344
|
+
if (outcome === "offline") break;
|
|
13345
|
+
}
|
|
13346
|
+
notify2();
|
|
13347
|
+
return result;
|
|
13348
|
+
}
|
|
13349
|
+
var listeners2 = /* @__PURE__ */ new Set();
|
|
13350
|
+
function notify2(change = "changed") {
|
|
13351
|
+
for (const l of listeners2) l(change);
|
|
13352
|
+
}
|
|
13353
|
+
function onCalendarOutboxChange(listener) {
|
|
13354
|
+
listeners2.add(listener);
|
|
13355
|
+
return () => listeners2.delete(listener);
|
|
13356
|
+
}
|
|
13357
|
+
async function wipeCalendarOffline() {
|
|
13358
|
+
await wipeStore(sealer2, store2);
|
|
13359
|
+
notify2();
|
|
13360
|
+
}
|
|
13361
|
+
async function claimCalendarOffline(userExtId) {
|
|
13362
|
+
const owner = await store2.getMeta("owner");
|
|
13363
|
+
if (owner && owner !== userExtId) await wipeCalendarOffline();
|
|
13364
|
+
await store2.setMeta("owner", userExtId);
|
|
13365
|
+
}
|
|
13366
|
+
|
|
13367
|
+
// src/calendar/offline/support.ts
|
|
13368
|
+
var calendarOfflineSupport = {
|
|
13369
|
+
async read(key, fetch2) {
|
|
13370
|
+
const cacheKey2 = JSON.stringify(key);
|
|
13371
|
+
if (!isCalendarOffline()) {
|
|
13372
|
+
try {
|
|
13373
|
+
const data = await fetch2();
|
|
13374
|
+
void writeCalendarCache(cacheKey2, data).catch(() => {
|
|
13375
|
+
});
|
|
13376
|
+
return data;
|
|
13377
|
+
} catch (err) {
|
|
13378
|
+
if (!isCalendarNetworkError(err)) throw err;
|
|
13379
|
+
}
|
|
13380
|
+
}
|
|
13381
|
+
const cached = await readCalendarCache(cacheKey2).catch(
|
|
13382
|
+
() => null
|
|
13383
|
+
);
|
|
13384
|
+
if (cached != null) return cached;
|
|
13385
|
+
throw new ArbiApiError("You are offline and this view is not saved on this device");
|
|
13386
|
+
},
|
|
13387
|
+
async write(op, send) {
|
|
13388
|
+
if (!isCalendarOffline()) return send();
|
|
13389
|
+
const { requestId } = await enqueueCalendarWrite(op);
|
|
13390
|
+
return { queued: true, requestId };
|
|
13391
|
+
},
|
|
13392
|
+
pending: listCalendarOutbox,
|
|
13393
|
+
subscribe: (listener) => onCalendarOutboxChange(() => listener())
|
|
13394
|
+
};
|
|
13395
|
+
var day = (date, offset) => {
|
|
13396
|
+
const t = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
13397
|
+
t.setUTCDate(t.getUTCDate() + offset);
|
|
13398
|
+
return t.toISOString();
|
|
13399
|
+
};
|
|
13400
|
+
var calendarOutboxExecutor = (arbi) => ({
|
|
13401
|
+
create: (body) => arbi.calendar.createEvent(body),
|
|
13402
|
+
update: (target, body) => arbi.calendar.updateEvent(target, body),
|
|
13403
|
+
delete: (target) => arbi.calendar.deleteEvent(target),
|
|
13404
|
+
respond: (params) => arbi.calendar.respond(params),
|
|
13405
|
+
/** No server dedupe: an in-doubt create counts as made if an event with its title and start exists. */
|
|
13406
|
+
async findCreated(_requestId, body) {
|
|
13407
|
+
const { title, start } = body;
|
|
13408
|
+
const date = (start?.date_time ?? start?.date ?? "").slice(0, 10);
|
|
13409
|
+
if (!title || !/^\d{4}-\d{2}-\d{2}$/.test(date)) return false;
|
|
13410
|
+
const events = await arbi.calendar.listEvents({
|
|
13411
|
+
start: day(date, -1),
|
|
13412
|
+
end: day(date, 2)
|
|
13413
|
+
});
|
|
13414
|
+
return events.some(
|
|
13415
|
+
(e) => e.title === title && (start?.date_time ? (e.start.date_time ?? "").slice(0, 16) === start.date_time.slice(0, 16) : e.start.date === start?.date)
|
|
13416
|
+
);
|
|
13417
|
+
}
|
|
13418
|
+
});
|
|
13419
|
+
var SYNC_CHANNEL = "arbi-calendar-outbox";
|
|
13420
|
+
function refreshCalendarQueries(queryClient) {
|
|
13421
|
+
void queryClient.invalidateQueries({ queryKey: calendarQueryKeys.all() });
|
|
13422
|
+
}
|
|
13423
|
+
async function replay(arbi, queryClient) {
|
|
13424
|
+
const result = await replayCalendarOutbox(calendarOutboxExecutor(arbi));
|
|
13425
|
+
if (result.sent || result.failed) {
|
|
13426
|
+
refreshCalendarQueries(queryClient);
|
|
13427
|
+
try {
|
|
13428
|
+
const channel2 = new BroadcastChannel(SYNC_CHANNEL);
|
|
13429
|
+
channel2.postMessage("synced");
|
|
13430
|
+
channel2.close();
|
|
13431
|
+
} catch {
|
|
13432
|
+
}
|
|
13433
|
+
}
|
|
13434
|
+
if (result.failed)
|
|
13435
|
+
notify.error(
|
|
13436
|
+
result.failed === 1 ? "A calendar change made offline could not be saved" : `${result.failed} calendar changes made offline could not be saved`
|
|
13437
|
+
);
|
|
13438
|
+
}
|
|
13439
|
+
function useCalendarOfflineRuntime() {
|
|
13440
|
+
const { status, serverSessionKey, user } = useArbiSession();
|
|
13441
|
+
const userExtId = user?.external_id;
|
|
13442
|
+
const authenticated = status === "signed-in";
|
|
13443
|
+
const hasSession = !!serverSessionKey;
|
|
13444
|
+
const arbi = useArbi();
|
|
13445
|
+
const ready = authenticated && hasSession && arbi.isLoggedIn;
|
|
13446
|
+
const queryClient = useQueryClient();
|
|
13447
|
+
useEffect(() => {
|
|
13448
|
+
if (!authenticated || !userExtId) return;
|
|
13449
|
+
claimCalendarOffline(userExtId).catch(
|
|
13450
|
+
(error) => logger.warn("[offline-calendar] could not bind the store", error)
|
|
13451
|
+
);
|
|
13452
|
+
}, [authenticated, userExtId]);
|
|
13453
|
+
useEffect(() => {
|
|
13454
|
+
if (!ready) return;
|
|
13455
|
+
const run2 = () => {
|
|
13456
|
+
if (!onlineManager.isOnline()) return;
|
|
13457
|
+
replay(arbi, queryClient).catch(
|
|
13458
|
+
(error) => logger.warn("[offline-calendar] replay failed", error)
|
|
13459
|
+
);
|
|
13460
|
+
};
|
|
13461
|
+
run2();
|
|
13462
|
+
let wasOnline = onlineManager.isOnline();
|
|
13463
|
+
const offOnline = onlineManager.subscribe((online) => {
|
|
13464
|
+
if (online && !wasOnline) run2();
|
|
13465
|
+
wasOnline = online;
|
|
13466
|
+
});
|
|
13467
|
+
let queued = null;
|
|
13468
|
+
const offOutbox = onCalendarOutboxChange((change) => {
|
|
13469
|
+
if (change !== "enqueued") return;
|
|
13470
|
+
if (queued) clearTimeout(queued);
|
|
13471
|
+
queued = setTimeout(run2, 2e3);
|
|
13472
|
+
});
|
|
13473
|
+
let channel2 = null;
|
|
13474
|
+
try {
|
|
13475
|
+
channel2 = new BroadcastChannel(SYNC_CHANNEL);
|
|
13476
|
+
channel2.onmessage = () => refreshCalendarQueries(queryClient);
|
|
13477
|
+
} catch {
|
|
13478
|
+
channel2 = null;
|
|
13479
|
+
}
|
|
13480
|
+
return () => {
|
|
13481
|
+
channel2?.close();
|
|
13482
|
+
offOnline();
|
|
13483
|
+
offOutbox();
|
|
13484
|
+
if (queued) clearTimeout(queued);
|
|
13485
|
+
};
|
|
13486
|
+
}, [ready, arbi, queryClient]);
|
|
13487
|
+
}
|
|
11922
13488
|
function useWorkspaceDocs(workspaceId) {
|
|
11923
13489
|
const { data: docs = [], isLoading, isError } = useDocuments(workspaceId);
|
|
11924
13490
|
const docIds = useMemo(
|
|
@@ -11947,7 +13513,7 @@ function useAiTask() {
|
|
|
11947
13513
|
artifactsRef.current = /* @__PURE__ */ new Map();
|
|
11948
13514
|
bufferRef.current = "";
|
|
11949
13515
|
}, []);
|
|
11950
|
-
const
|
|
13516
|
+
const run2 = useCallback(
|
|
11951
13517
|
async (question, docIds, opts) => {
|
|
11952
13518
|
setStatus("running");
|
|
11953
13519
|
setText("");
|
|
@@ -12015,7 +13581,7 @@ function useAiTask() {
|
|
|
12015
13581
|
error,
|
|
12016
13582
|
responseId,
|
|
12017
13583
|
artifacts,
|
|
12018
|
-
run,
|
|
13584
|
+
run: run2,
|
|
12019
13585
|
reset,
|
|
12020
13586
|
isRunning: status === "running",
|
|
12021
13587
|
isDone: status === "done"
|
|
@@ -12453,13 +14019,13 @@ function useSemanticSearch(docIds = [], options = {}) {
|
|
|
12453
14019
|
clearSearchSession(void 0, session);
|
|
12454
14020
|
return;
|
|
12455
14021
|
}
|
|
12456
|
-
const
|
|
14022
|
+
const run2 = session.getState().run + 1;
|
|
12457
14023
|
const searched = workspaceId ?? session.getState().workspaceId ?? void 0;
|
|
12458
14024
|
session.setState((s) => ({
|
|
12459
14025
|
query: trimmed,
|
|
12460
14026
|
status: "running",
|
|
12461
14027
|
error: null,
|
|
12462
|
-
run,
|
|
14028
|
+
run: run2,
|
|
12463
14029
|
workspaceId: workspaceId ?? s.workspaceId
|
|
12464
14030
|
}));
|
|
12465
14031
|
try {
|
|
@@ -12469,10 +14035,10 @@ function useSemanticSearch(docIds = [], options = {}) {
|
|
|
12469
14035
|
...model ? { model } : {},
|
|
12470
14036
|
...searched ? { workspaceId: searched } : {}
|
|
12471
14037
|
});
|
|
12472
|
-
if (session.getState().run !==
|
|
14038
|
+
if (session.getState().run !== run2) return;
|
|
12473
14039
|
session.setState({ results: searchChunks(result), searched: true, status: "done" });
|
|
12474
14040
|
} catch (error) {
|
|
12475
|
-
if (session.getState().run !==
|
|
14041
|
+
if (session.getState().run !== run2) return;
|
|
12476
14042
|
session.setState({
|
|
12477
14043
|
status: "error",
|
|
12478
14044
|
error: error instanceof Error ? error.message : "Search failed"
|
|
@@ -12675,7 +14241,7 @@ var CitationRendererComponent = ({
|
|
|
12675
14241
|
useCitationState,
|
|
12676
14242
|
onCloseCitation,
|
|
12677
14243
|
onDismissCitation,
|
|
12678
|
-
notify:
|
|
14244
|
+
notify: notify3,
|
|
12679
14245
|
docTitles
|
|
12680
14246
|
}) => {
|
|
12681
14247
|
const chunkMap = useMemo(() => {
|
|
@@ -12719,7 +14285,7 @@ var CitationRendererComponent = ({
|
|
|
12719
14285
|
useCitationState,
|
|
12720
14286
|
onCloseCitation,
|
|
12721
14287
|
onDismissCitation,
|
|
12722
|
-
notify:
|
|
14288
|
+
notify: notify3
|
|
12723
14289
|
}),
|
|
12724
14290
|
[
|
|
12725
14291
|
citations,
|
|
@@ -12732,7 +14298,7 @@ var CitationRendererComponent = ({
|
|
|
12732
14298
|
useCitationState,
|
|
12733
14299
|
onCloseCitation,
|
|
12734
14300
|
onDismissCitation,
|
|
12735
|
-
|
|
14301
|
+
notify3
|
|
12736
14302
|
]
|
|
12737
14303
|
);
|
|
12738
14304
|
return /* @__PURE__ */ jsx(CitationAnchorContext.Provider, { value: anchorContext, children: /* @__PURE__ */ jsx("div", { className: cn("citation-container", className), "data-testid": "citation-renderer", children: renderMarkdown({
|
|
@@ -12791,7 +14357,7 @@ var CitationPopover = ({
|
|
|
12791
14357
|
useCitationState,
|
|
12792
14358
|
onCloseCitation,
|
|
12793
14359
|
onDismissCitation,
|
|
12794
|
-
notify:
|
|
14360
|
+
notify: notify3,
|
|
12795
14361
|
children
|
|
12796
14362
|
}) => {
|
|
12797
14363
|
const { activeCitationKey, viewingPassageId, isOnConversationPage } = useCitationState();
|
|
@@ -12865,11 +14431,11 @@ var CitationPopover = ({
|
|
|
12865
14431
|
const source = [firstTitle, firstPage != null ? `p.${firstPage}` : null].filter(Boolean).join(", ");
|
|
12866
14432
|
try {
|
|
12867
14433
|
await navigator.clipboard.writeText(source ? `\u201C${firstText}\u201D (${source})` : firstText);
|
|
12868
|
-
|
|
14434
|
+
notify3?.("Citation copied", "success");
|
|
12869
14435
|
} catch {
|
|
12870
|
-
|
|
14436
|
+
notify3?.("Could not copy the citation", "error");
|
|
12871
14437
|
}
|
|
12872
|
-
}, [firstText, firstTitle, firstPage,
|
|
14438
|
+
}, [firstText, firstTitle, firstPage, notify3]);
|
|
12873
14439
|
const items = isStreaming || chunks.length === 0 ? [] : [
|
|
12874
14440
|
{
|
|
12875
14441
|
id: "open",
|
|
@@ -14582,12 +16148,12 @@ function annotateThread(messages, accessors, runWindowMs = RUN_WINDOW_MS) {
|
|
|
14582
16148
|
return messages.map((message, i) => {
|
|
14583
16149
|
const next = messages[i + 1];
|
|
14584
16150
|
const prev = messages[i - 1];
|
|
14585
|
-
const
|
|
14586
|
-
const continuesIntoNext = !!next && senderId(next) === senderId(message) && dayKey(createdAt(next)) ===
|
|
16151
|
+
const day2 = dayKey(createdAt(message));
|
|
16152
|
+
const continuesIntoNext = !!next && senderId(next) === senderId(message) && dayKey(createdAt(next)) === day2 && Date.parse(createdAt(next)) - Date.parse(createdAt(message)) < runWindowMs;
|
|
14587
16153
|
return {
|
|
14588
16154
|
message,
|
|
14589
16155
|
isLastOfRun: !continuesIntoNext,
|
|
14590
|
-
isFirstOfDay: !prev || dayKey(createdAt(prev)) !==
|
|
16156
|
+
isFirstOfDay: !prev || dayKey(createdAt(prev)) !== day2
|
|
14591
16157
|
};
|
|
14592
16158
|
});
|
|
14593
16159
|
}
|
|
@@ -15032,6 +16598,6 @@ function useRateSupportTicket() {
|
|
|
15032
16598
|
});
|
|
15033
16599
|
}
|
|
15034
16600
|
|
|
15035
|
-
export { AGENT_CONTROL_COMMANDS, AGGREGATE_FIELDS, ALLOWED_PORTAL_ORIGINS, ARCHIVE_POLL_MS, ASSISTANT_MENTION_LABEL, AUTO_CHAT_MODEL, ActivityFeed, AiActionCard, AiAnswer, AppleIcon, ArbiBotIcon, ArbiHost2 as ArbiHost, ArbiWebSocketProvider, ArtifactCanvas, BOOT_GATE_TIMEOUT_MS, BOX_SEAT_REFUSED, BOX_SEAT_REFUSED_ACTION, BOX_SEAT_REFUSED_TITLE, BUILTIN_DATA_SOURCES, CONVERSATION_SWITCH_MEASURE, ChatBubble, CitationRenderer, CommandPalette, CommandPaletteProvider, ConversationList, ConversationScope, DATABASE_RELATION_PICK_LIMIT, DATABASE_ROWS_PAGE, DEFAULT_ASPECT_OPTIONS, DEFAULT_CONVERSATIONS_SOURCE_LABELS, DEFAULT_DATABASE_SOURCE_LABELS, DEFAULT_DOCUMENTS_SOURCE_LABELS, DEFAULT_PASSWORD_CHANGE_FORM_MESSAGES, DEFAULT_PROJECTS_SOURCE_LABELS, DEFAULT_USAGE_SOURCE_LABELS, DEFAULT_WORKSPACES_SOURCE_LABELS, DateSeparator, DmAvatar, DmComposer, DocQaPanel, DocSearch, DocumentCardMenu, DocumentReview, DraftTitleControls, EMPTY_GROUP_LABEL, FACTS_SEARCH_VIEW, FIRST_TOKEN_MEASURE, FileIcon, GitHubIcon, ImageCropModal, ImageCropper, LIVE_UPDATE_TYPES, LinkedInIcon, MAIL_DRAFT_REPLACED_EVENT, MAX_DASHBOARD_SPEC_CHARS, MAX_SERIES, MIN_NARRATION_DWELL_MS, MODEL_GROUP_LABELS, MailOfflineUnavailableError, MessageArtifactCard, PENDING_SYNC_PREDICATE, POLL_EVERY_MS, PhotoCapture, RUN_WINDOW_MS, SIGNATURE_NOTIFICATION_TYPES, SLOW_AFTER_MS, STILL_WORKING, StorageActionError, TRANSFER_POLL_MS, TaskThreadPendingError, TypingIndicator, UPLOAD_ACCEPT, UPLOAD_MAX_FILE_SIZE, VideoCapture, VoiceNoteRecorder, WORK_NOTIFICATION_LABELS, WORK_OFFLINE_VIEW, __resetPendingFinalCommitsForTests, __resetStreamingTokenBufferForTests, accountPath, addAssistantMessageToCache, addUserMessageToCache, adoptNewChatDocument, aggregateDataset, aliasTakenMessage, allConversationThreads, annotateThread, announceMailDraftReplaced, appendTokenToMessage, applyConversationEvent, applyFinalContextToMessage, applyLiveUpdate, applyNotificationRow, applyOptimisticDocumentEdit, applyOutputItemToMessage, applyTaskEvent, arbiMailClient, archiveName, archivedWorkspaceMessage, assistantLabels, bindThreadDocument, boundDocumentOf, bucketOf, bucketRange, buildDocumentMetadataRows, buildMentionCandidates, buildPendingFirstTurn, buildPendingSyncItem, buildWorkspaceArchive, cacheThreadBinding, cachedAssistantMode, cachedMentionCandidates, cachedQuotaUsedUp, calendarNotificationMeta, callNotificationMeta, captureFrameAsBlob, claimOfflineQueue, clearLoadingState, clearUserInputRequest, commitFinalMessage, completeQueuedUserMessageByContent, computeOnline, connectOAuthCalendarAccount, connectorErrorStatus, connectorReturnTo, createArbiQueryClient, createConversationsSource, createDataSourceRegistry, createDatabaseSource, createDocumentsSource, createMailClient, createMemoryDashboardStore, createProjectsSource, createSkillPack, createStreamHandlers, createUsageSource, createWorkspaceSwitch, createWorkspacesSource, cropImage, dayKey, defaultMailAlias, defineRowsSource, deploymentUserMeta, describeActivity, describeCalendarNotification, describeServerNotification, discardStreamingTokensForMessage, disconnectAttachedRun, documentActivityMeta, documentActivityPhrase, documentDisplayName, docxMentionMeta, downloadRecoveryFile, ensureAssistantMessageLoading, extractZipFile, fallbackFileName, fileAttachment, filesFromDrop, fillTimeGaps, filterRows, flushStreamingTokens, forgetUploadedDocumentFolder, formatConfigValidationError, formatIdentityPrice, getInterjectionText, getNotificationTypeMeta, getOfflineDocument, getOrphanedLastMessage, getRelatedLabels, getRelatedTotal, getRunStartedAt, getThreadsQueryKey, getWorkspaceActionText, getWorkspaceKind, getWorkspaceSentPhrase, hasPendingFinalCommit, hasVideoTrack, identityStepFor, inputRequestTitle, invalidateCalendarEvents, invalidateCalendars, invalidateConversationActivity, isActivityKey, isAliasTakenError, isArchiveMoving, isCommentToPeople, isDeploymentAdmin, isDeploymentUserNotification, isFullConfig, isMessageRunning, isNotificationRow, isOrphanedUserMessage, isQueuedCalendarWrite, isReplayableArbiMutation, isSignatureNotificationType, isTableViewOf, isTransferFinished, isTransferResumable, isWorkingOffline, isWorkspaceArchivedError, isWorkspaceNotificationType, isZipFile, loadOfflineDocument, mailDraftIdFromContent, mailJson, markMessageAsFailed, markNarrationSettled, migrateLegacyThreadDocuments, mountDiditVerification, nextAliasSuggestion, notificationUserName, openAccessRequest, parseActivityContent, parseDashboardSpec, parseDeleteAccountBlockers, pendingChatModelSave, peopleDirectory, personOfUser, pickRecentWorkspaces, prepareUploadFiles, queryOfChart, recordsOf, redlineStats, refuseChatWhenOffline, registerArbiMutationDefaults, registerAttachedRun, removeSettledNarration, requestPersistentStorage, resetCalendarAccountSyncState, resetMessageContent, resolveActiveWorkspace, resolveConfigModel, resolveReplyTarget, rollbackOptimisticDocumentEdit, safePortalUrl, serializeDashboardSpec, serverNotificationBellEntry, setMessageContent, setUserInputRequest, setWorkOfflineIn, signatureNotificationPhrase, signatureRequestMeta, subscribeDocumentReads, supportsHouseTemplates, textToArtifact, threadBinding, threadEndingAtContains, titleFromQuestion, toChatCitations, toNotificationPreferences, toRedlineMarkdown, toShellWorkspace, updateMessageWithMetadata, updateUserMessageInCache, updateUserMessageStateInCache, uploadFolderOf, uploadedDocumentFolder, useActOnStep, useActiveConfigId, useAddChatMembers, useAddDatabaseRows, useAddHolidayCalendar, useAddProjectAddonBlock, useAddWorkspaceMembers, useAddonQuote, useAgentChats, useAgentConfig, useAgentDmStatus, useAgentModels, useAgentRunControl, useAgentSession, useAgentWorkspaces, useAiTask, useAnswerAgentQuestion, useApplyChatModel, useArbiConnected, useArbiShellWorkspaces, useArbiWebSocket, useArbiWsContext, useArbiWsMessage, useArbiWsStatus, useArchiveProgressPolling, useArchiveWorkspace, useAskAgain, useAssignDocTags, useAssistantName, useAuthorizeLimits, useAutoSendDraft, useBookingPages, useBootTimeout, useBranchNavigation, useCalDavProfiles, useCalendarAccounts, useCalendarEvent, useCalendarEventByUid, useCalendarEvents, useCalendarInvitations, useCalendarOutbox, useCalendarOverlays, useCalendarSettings, useCalendarShares, useCalendarSupport, useCalendars, useCameraStream, useCanEditShared, useCancelDriveSync, useCancelMailboxSync, useCancelManyPendingSync, useCancelProjectSubscription, useCancelWorkspaceTransfer, useChangeProjectPlan, useChargeWatch, useChatArrivals, useChatPost, useChatSummaries, useChatThread, useChatUnreadTotal, useChats, useClaimNativeAlias, useClaimSecureMail, useCloseAgentSession, useCommandPalette, useConfigSchema, useConfirmEmailChange, useConnectCalDavAccount, useConnectMailAccount, useConnectOAuthAccount, useConnectRunStream, useConnectorAutoSync, useConnectorCatalog, useConnectorVendorVisibility, useContacts, useConversation, useConversationActions, useConversationActivity, useConversationData, useConversationDocuments, useConversationIsShared, useConversationItems, useConversationPendingAuth, useConversationSearch, useConversationSettings, useConversationTurn, useConversationUsage, useConversationWorkspace, useCreateAgent, useCreateCalendar, useCreateContact, useCreateDatabase, useCreateEvent, useCreateFact, useCreateFacts, useCreateGroupChat, useCreateProject, useCreateProjectCheckout, useCreateRoom, useCreateSupportTicket, useCreateWork, useCreateWorkflow, useCreateWorkspace, useCurrentSessionId, useCurrentWorkspace, useDashboardStore, useDataSources, useDatabase, useDatabaseAggregate, useDatabaseDashboardStore, useDatabaseRows, useDatabases, useDecideCounter, useDeleteAccount, useDeleteAccountFlow, useDeleteBookingPage, useDeleteCalendar, useDeleteChatMessage, useDeleteConfig, useDeleteContact, useDeleteConversation, useDeleteDashboard, useDeleteDatabase, useDeleteDatabaseRows, useDeleteDocuments, useDeleteEvent, useDeleteFact, useDeleteMessage, useDeleteNotifications, useDeleteProject, useDeleteSkill, useDeleteWorkflow, useDeleteWorkspaces, useDirectModelUsage, useDisconnectCalendarAccount, useDisconnectConnector, useDisconnectMailAccount, useDisconnectSlack, useDismissedTips, useDocumentAutoAdd, useDocumentHostActions, useDocumentPendingSync, useDocumentThumbnails, useDocumentsSupport, useDownloadDocuments, useDriveFolderChildren, useDriveFolderDocCount, useDriveFolders, useDriveSyncEstimate, useDriveSyncProgress, useEmailChange, useEnableDriveSync, useEnableMailboxSync, useExportDatabase, useFacts, useFactsSearch, useFetchCalendarEvent, useFilteredProjects, useFinaliseTranslation, useFindTime, useFollowThread, useFreeBusy, useGenerateAgentAvatar, useGenerateDocTags, useGrantAgentAccess, useHolidayCountries, useHouseTemplateCandidates, useIdentityVerification, useIdentityVerificationFlow, useImportCalendar, useImportContacts, useImportDatabase, useImportDatabaseRows, useInstallSkillPack, useInvalidateAfterTransfer, useInvalidateProjects, useInvalidateStorage, useInviteContacts, useIsSlow, useIsWorkspaceOwner, useKeepDocumentsOffline, useLinkContact, useLinkMentions, useMailAccounts, useMailAttachmentText, useMailCache, useMailClient, useMailDraftCompose, useMailDraftEditor, useMailDraftReplaced, useMailFolders, useMailMessage, useMailMessages, useMailOnline, useMailPolicy, useMailProjects, useMailUnlock, useMailboxFolders, useMailboxSyncProgress, useMarkChatRead, useMarkDocumentShared, useMarkNotificationsRead, useMcpTools, useMentionCandidates, useMessageTimer, useModelConnections, useMoveEvent, useMoveWorkspaceToProject, useMyConnectors, useNotificationPreferences, useNotifications, useNotifyConversation, useOfflineDocumentIds, useOpenSelectedWorkspace, useOpenViewerDocumentId, usePanelDataset, usePassageQuotes, usePasswordChange, usePasswordChangeForm, usePendingSyncCounts, usePendingSyncItems, usePeople, usePeopleSearch, usePlanQuote, usePlanWork, usePlanWorkspaceTransfer, usePreviewProjectPlanChange, useProcessingTask, useProcessingTasks, useProducts, useProjectBillingPortal, useProjectConversationUsage, useProjectEvents, usePublishCalendar, useRateSupportTicket, useReconnect, useRefreshCalendar, useRefreshConnectorsOnRunEnd, useRefreshProject, useRelatedDocuments, useRemoveChatMember, useRemoveDocTags, useRemoveDocumentsOffline, useRemoveModelConnection, useRemoveWorkspaceMembers, useRenameChat, useRenameFolder, useRequestEmailChange, useRequestWorkspaceAccess, useRespondToEvent, useResumeCalendarAccountConnect, useResumeWorkspaceTransfer, useRevokeAgentAccess, useRooms, useRunConversation, useRunWorkflow, useSaveAgentConfig, useSaveBookingPage, useSaveCalendarSettings, useSaveConfig, useSaveConfigField, useSaveDashboard, useSaveMailDraft, useSaveModelConnection, useSaveSkill, useSavedDashboards, useSearchContacts, useSearchDatabaseRows, useSearchFacts, useSemanticSearch, useSendBackStep, useSendMail, useSessions, useSetDeploymentHouseTemplate, useSetSkillVisibility, useSetWorkspaceHouseTemplate, useSetWorkspaceMemberRoles, useShareCalendar, useShellWorkspaces, useSignOut, useSkillSource, useSkills, useSlackConnection, useStartAgentSession, useStartCalendarAccountOAuth, useStartTaskQuery, useStartWorkspaceTransfer, useStorageActions, useStorageItemNames, useStorageItems, useStorageProjects, useStorageSnapshots, useStreamHandlers, useSubscribeCalendar, useSupportTicket, useSupportTickets, useSwitchWorkspace, useSyncAccountsOnOpen, useSyncCalendarAccount, useTerminateSession, useThinkingContent, useThreadDocument, useTriggerDriveSync, useTriggerMailboxSync, useUnarchiveWorkspace, useUnlockModelConnections, useUnreadByPeer, useUnshareCalendar, useUpdateCalendar, useUpdateCalendarAccount, useUpdateContact, useUpdateConversation, useUpdateDatabase, useUpdateDatabaseRow, useUpdateDocTag, useUpdateDocuments, useUpdateEvent, useUpdateFact, useUpdateMailMessage, useUpdateProjectAddonSettings, useUpdateProjectFields, useUpdateProjectNextPeriodAddons, useUpdateWorkflow, useUpdateWorkspace, useUploadDocuments, useUploadFiles, useUploadFromUrls, useUploadSkillFiles, useUploadUrls, useVideoRecorder, useWorkItems, useWorkNotifications, useWorkOffline, useWorkflowRuns, useWorkflows, useWorkspaceArchive, useWorkspaceDocs, useWorkspaceExportDownload, useWorkspaceNotificationLevel, useWorkspaceProcessing, useWorkspaceRole, useWorkspaceTransfer, useWorkspaceTransfers, useWorkspaceTrash, useWorkspaceUsers, userDisplayName, userInitials, viewsOfTable, waitForResponseId, wipeOfflineDocuments, withRelativePath, workflowStepOf, workspaceHouseTemplateId };
|
|
16601
|
+
export { AGENT_CONTROL_COMMANDS, AGGREGATE_FIELDS, ALLOWED_PORTAL_ORIGINS, ARCHIVE_POLL_MS, ASSISTANT_MENTION_LABEL, AUTO_CHAT_MODEL, ActivityFeed, AiActionCard, AiAnswer, AppleIcon, ArbiBotIcon, ArbiHost2 as ArbiHost, ArbiWebSocketProvider, ArtifactCanvas, BOOT_GATE_TIMEOUT_MS, BOX_SEAT_REFUSED, BOX_SEAT_REFUSED_ACTION, BOX_SEAT_REFUSED_TITLE, BUILTIN_DATA_SOURCES, CONVERSATION_SWITCH_MEASURE, ChatBubble, CitationRenderer, CommandPalette, CommandPaletteProvider, ConversationList, ConversationScope, DATABASE_RELATION_PICK_LIMIT, DATABASE_ROWS_PAGE, DEFAULT_ASPECT_OPTIONS, DEFAULT_CONVERSATIONS_SOURCE_LABELS, DEFAULT_DATABASE_SOURCE_LABELS, DEFAULT_DOCUMENTS_SOURCE_LABELS, DEFAULT_PASSWORD_CHANGE_FORM_MESSAGES, DEFAULT_PROJECTS_SOURCE_LABELS, DEFAULT_USAGE_SOURCE_LABELS, DEFAULT_WORKSPACES_SOURCE_LABELS, DateSeparator, DmAvatar, DmComposer, DocQaPanel, DocSearch, DocumentCardMenu, DocumentReview, DraftTitleControls, EMPTY_GROUP_LABEL, FACTS_SEARCH_VIEW, FIRST_TOKEN_MEASURE, FileIcon, GitHubIcon, ImageCropModal, ImageCropper, LIVE_UPDATE_TYPES, LinkedInIcon, MAIL_DRAFT_REPLACED_EVENT, MAX_DASHBOARD_SPEC_CHARS, MAX_SERIES, MIN_NARRATION_DWELL_MS, MODEL_GROUP_LABELS, MailOfflineUnavailableError, MessageArtifactCard, OUTBOX_FOLDER, PENDING_SYNC_PREDICATE, POLL_EVERY_MS, PhotoCapture, RUN_WINDOW_MS, SIGNATURE_NOTIFICATION_TYPES, SLOW_AFTER_MS, STILL_WORKING, StorageActionError, TRANSFER_POLL_MS, TaskThreadPendingError, TypingIndicator, UPLOAD_ACCEPT, UPLOAD_MAX_FILE_SIZE, VideoCapture, VoiceNoteRecorder, WORK_NOTIFICATION_LABELS, WORK_OFFLINE_VIEW, __resetPendingFinalCommitsForTests, __resetStreamingTokenBufferForTests, accountPath, addAssistantMessageToCache, addUserMessageToCache, adoptNewChatDocument, aggregateDataset, aliasTakenMessage, allConversationThreads, annotateThread, announceMailDraftReplaced, appendTokenToMessage, applyConversationEvent, applyFinalContextToMessage, applyLiveUpdate, applyNotificationRow, applyOptimisticDocumentEdit, applyOutputItemToMessage, applyTaskEvent, arbiMailClient, archiveName, archivedWorkspaceMessage, assistantLabels, bindThreadDocument, boundDocumentOf, bucketOf, bucketRange, buildDocumentMetadataRows, buildMentionCandidates, buildPendingFirstTurn, buildPendingSyncItem, buildWorkspaceArchive, cacheThreadBinding, cachedAssistantMode, cachedMentionCandidates, cachedQuotaUsedUp, calendarNotificationMeta, calendarOfflineSupport, callNotificationMeta, captureFrameAsBlob, claimOfflineQueue, clearLoadingState, clearUserInputRequest, commitFinalMessage, completeQueuedUserMessageByContent, computeOnline, confirmLosingOfflineMail, connectOAuthCalendarAccount, connectorErrorStatus, connectorReturnTo, createArbiQueryClient, createConversationsSource, createDataSourceRegistry, createDatabaseSource, createDocumentsSource, createMailClient, createMemoryDashboardStore, createProjectsSource, createSkillPack, createStreamHandlers, createUsageSource, createWorkspaceSwitch, createWorkspacesSource, cropImage, dayKey, defaultMailAlias, defineRowsSource, deleteLocalDraft, deleteOutboxItem, deploymentUserMeta, describeActivity, describeCalendarNotification, describeServerNotification, discardStreamingTokensForMessage, disconnectAttachedRun, documentActivityMeta, documentActivityPhrase, documentDisplayName, documentMailAttachment, docxMentionMeta, downloadRecoveryFile, enqueueOutbox, ensureAssistantMessageLoading, extractZipFile, fallbackFileName, fileAttachment, filesFromDrop, fillTimeGaps, filterRows, flushStreamingTokens, forgetUploadedDocumentFolder, formatConfigValidationError, formatIdentityPrice, getInterjectionText, getNotificationTypeMeta, getOfflineDocument, getOrphanedLastMessage, getRelatedLabels, getRelatedTotal, getRunStartedAt, getThreadsQueryKey, getWorkspaceActionText, getWorkspaceKind, getWorkspaceSentPhrase, hasPendingFinalCommit, hasVideoTrack, identityStepFor, inputRequestTitle, invalidateCalendarEvents, invalidateCalendars, invalidateConversationActivity, isActivityKey, isAliasTakenError, isArchiveMoving, isCommentToPeople, isDeploymentAdmin, isDeploymentUserNotification, isFullConfig, isMessageRunning, isNetworkFailure, isNotificationRow, isOrphanedUserMessage, isQueuedCalendarWrite, isReplayableArbiMutation, isSignatureNotificationType, isTableViewOf, isTransferFinished, isTransferResumable, isWorkingOffline, isWorkspaceArchivedError, isWorkspaceNotificationType, isZipFile, loadOfflineDocument, loadAttachment as loadOfflineMailAttachment, mailDraftIdFromContent, mailJson, markMessageAsFailed, markNarrationSettled, migrateLegacyThreadDocuments, mountDiditVerification, nextAliasSuggestion, notificationUserName, offlineMailReads, openAccessRequest, parseActivityContent, parseDashboardSpec, parseDeleteAccountBlockers, pendingChatModelSave, peopleDirectory, personOfUser, pickRecentWorkspaces, prepareUploadFiles, queryOfChart, recordsOf, redlineStats, refuseChatWhenOffline, registerArbiMutationDefaults, registerAttachedRun, removeSettledNarration, requestPersistentStorage, resetCalendarAccountSyncState, resetMessageContent, resolveActiveWorkspace, resolveConfigModel, resolveReplyTarget, retryOutboxItem, rollbackOptimisticDocumentEdit, safePortalUrl, saveLocalDraft, serializeDashboardSpec, serverNotificationBellEntry, setMessageContent, setUserInputRequest, setWorkOfflineIn, signatureNotificationPhrase, signatureRequestMeta, subscribeDocumentReads, supportsHouseTemplates, textToArtifact, threadBinding, threadEndingAtContains, titleFromQuestion, toChatCitations, toNotificationPreferences, toRedlineMarkdown, toShellWorkspace, updateMessageWithMetadata, updateUserMessageInCache, updateUserMessageStateInCache, uploadFolderOf, uploadedDocumentFolder, useActOnStep, useActiveConfigId, useAddChatMembers, useAddDatabaseRows, useAddHolidayCalendar, useAddProjectAddonBlock, useAddWorkspaceMembers, useAddonQuote, useAgentChats, useAgentConfig, useAgentDmStatus, useAgentModels, useAgentRunControl, useAgentSession, useAgentWorkspaces, useAiTask, useAnswerAgentQuestion, useApplyChatModel, useArbiConnected, useArbiShellWorkspaces, useArbiWebSocket, useArbiWsContext, useArbiWsMessage, useArbiWsStatus, useArchiveProgressPolling, useArchiveWorkspace, useAskAgain, useAssignDocTags, useAssistantName, useAuthorizeLimits, useAutoSendDraft, useBookingPages, useBootTimeout, useBranchNavigation, useCalDavProfiles, useCalendarAccounts, useCalendarEvent, useCalendarEventByUid, useCalendarEvents, useCalendarInvitations, useCalendarOfflineRuntime, useCalendarOutbox, useCalendarOverlays, useCalendarSettings, useCalendarShares, useCalendarSupport, useCalendars, useCameraStream, useCanEditShared, useCancelDriveSync, useCancelMailboxSync, useCancelManyPendingSync, useCancelProjectSubscription, useCancelWorkspaceTransfer, useChangeProjectPlan, useChargeWatch, useChatArrivals, useChatPost, useChatSummaries, useChatThread, useChatUnreadTotal, useChats, useClaimNativeAlias, useClaimSecureMail, useCloseAgentSession, useCommandPalette, useConfigSchema, useConfirmEmailChange, useConnectCalDavAccount, useConnectMailAccount, useConnectOAuthAccount, useConnectRunStream, useConnectorAutoSync, useConnectorCatalog, useConnectorVendorVisibility, useContacts, useConversation, useConversationActions, useConversationActivity, useConversationData, useConversationDocuments, useConversationIsShared, useConversationItems, useConversationPendingAuth, useConversationSearch, useConversationSettings, useConversationTurn, useConversationUsage, useConversationWorkspace, useCreateAgent, useCreateCalendar, useCreateContact, useCreateDatabase, useCreateEvent, useCreateFact, useCreateFacts, useCreateGroupChat, useCreateProject, useCreateProjectCheckout, useCreateRoom, useCreateSupportTicket, useCreateWork, useCreateWorkflow, useCreateWorkspace, useCurrentSessionId, useCurrentWorkspace, useDashboardStore, useDataSources, useDatabase, useDatabaseAggregate, useDatabaseDashboardStore, useDatabaseRows, useDatabases, useDecideCounter, useDeleteAccount, useDeleteAccountFlow, useDeleteBookingPage, useDeleteCalendar, useDeleteChatMessage, useDeleteConfig, useDeleteContact, useDeleteConversation, useDeleteDashboard, useDeleteDatabase, useDeleteDatabaseRows, useDeleteDocuments, useDeleteEvent, useDeleteFact, useDeleteMessage, useDeleteNotifications, useDeleteProject, useDeleteSkill, useDeleteWorkflow, useDeleteWorkspaces, useDirectModelUsage, useDisconnectCalendarAccount, useDisconnectConnector, useDisconnectMailAccount, useDisconnectSlack, useDismissedTips, useDocumentAutoAdd, useDocumentHostActions, useDocumentPendingSync, useDocumentThumbnails, useDocumentsSupport, useDownloadDocuments, useDriveFolderChildren, useDriveFolderDocCount, useDriveFolders, useDriveSyncEstimate, useDriveSyncProgress, useEmailChange, useEnableDriveSync, useEnableMailboxSync, useExportDatabase, useFacts, useFactsSearch, useFetchCalendarEvent, useFilteredProjects, useFinaliseTranslation, useFindTime, useFlushOutbox, useFollowThread, useFreeBusy, useGenerateAgentAvatar, useGenerateDocTags, useGrantAgentAccess, useHolidayCountries, useHouseTemplateCandidates, useIdentityVerification, useIdentityVerificationFlow, useImportCalendar, useImportContacts, useImportDatabase, useImportDatabaseRows, useInstallSkillPack, useInvalidateAfterTransfer, useInvalidateProjects, useInvalidateStorage, useInviteContacts, useIsSlow, useIsWorkspaceOwner, useKeepDocumentsOffline, useLinkContact, useLinkMentions, useLocalDrafts, useMailAccounts, useMailAttachmentText, useMailCache, useMailClient, useMailDraftCompose, useMailDraftEditor, useMailDraftReplaced, useMailFolders, useMailMessage, useMailMessages, useMailOnline, useMailPolicy, useMailProjects, useMailUnlock, useMailboxFolders, useMailboxSyncProgress, useMarkChatRead, useMarkDocumentShared, useMarkNotificationsRead, useMcpTools, useMentionCandidates, useMessageTimer, useModelConnections, useMoveEvent, useMoveWorkspaceToProject, useMyConnectors, useNotificationPreferences, useNotifications, useNotifyConversation, useOfflineDocumentIds, useOfflineMailRuntime, useOfflineMailSettings, useOfflineMailStatus, useOpenSelectedWorkspace, useOpenViewerDocumentId, useOutbox, usePanelDataset, usePassageQuotes, usePasswordChange, usePasswordChangeForm, usePendingSyncCounts, usePendingSyncItems, usePeople, usePeopleSearch, usePlanQuote, usePlanWork, usePlanWorkspaceTransfer, usePreviewProjectPlanChange, useProcessingTask, useProcessingTasks, useProducts, useProjectBillingPortal, useProjectConversationUsage, useProjectEvents, usePublishCalendar, useRateSupportTicket, useReconnect, useRefreshCalendar, useRefreshConnectorsOnRunEnd, useRefreshProject, useRelatedDocuments, useRemoveChatMember, useRemoveDocTags, useRemoveDocumentsOffline, useRemoveModelConnection, useRemoveWorkspaceMembers, useRenameChat, useRenameFolder, useRequestEmailChange, useRequestWorkspaceAccess, useRespondToEvent, useResumeCalendarAccountConnect, useResumeWorkspaceTransfer, useRevokeAgentAccess, useRooms, useRunConversation, useRunWorkflow, useSaveAgentConfig, useSaveBookingPage, useSaveCalendarSettings, useSaveConfig, useSaveConfigField, useSaveDashboard, useSaveMailDraft, useSaveModelConnection, useSaveSkill, useSavedDashboards, useSearchContacts, useSearchDatabaseRows, useSearchFacts, useSemanticSearch, useSendBackStep, useSendMail, useSessions, useSetDeploymentHouseTemplate, useSetSkillVisibility, useSetWorkspaceHouseTemplate, useSetWorkspaceMemberRoles, useShareCalendar, useShellWorkspaces, useSignOut, useSkillSource, useSkills, useSlackConnection, useStartAgentSession, useStartCalendarAccountOAuth, useStartTaskQuery, useStartWorkspaceTransfer, useStorageActions, useStorageItemNames, useStorageItems, useStorageProjects, useStorageSnapshots, useStreamHandlers, useSubscribeCalendar, useSupportTicket, useSupportTickets, useSwitchWorkspace, useSyncAccountsOnOpen, useSyncCalendarAccount, useSyncOfflineMailbox, useTerminateSession, useThinkingContent, useThreadDocument, useTriggerDriveSync, useTriggerMailboxSync, useUnarchiveWorkspace, useUnlockModelConnections, useUnreadByPeer, useUnshareCalendar, useUpdateCalendar, useUpdateCalendarAccount, useUpdateContact, useUpdateConversation, useUpdateDatabase, useUpdateDatabaseRow, useUpdateDocTag, useUpdateDocuments, useUpdateEvent, useUpdateFact, useUpdateMailMessage, useUpdateProjectAddonSettings, useUpdateProjectFields, useUpdateProjectNextPeriodAddons, useUpdateWorkflow, useUpdateWorkspace, useUploadDocuments, useUploadFiles, useUploadFromUrls, useUploadSkillFiles, useUploadUrls, useVideoRecorder, useWorkItems, useWorkNotifications, useWorkOffline, useWorkflowRuns, useWorkflows, useWorkspaceArchive, useWorkspaceDocs, useWorkspaceExportDownload, useWorkspaceNotificationLevel, useWorkspaceProcessing, useWorkspaceRole, useWorkspaceTransfer, useWorkspaceTransfers, useWorkspaceTrash, useWorkspaceUsers, userDisplayName, userInitials, viewsOfTable, waitForResponseId, wipeCalendarOffline, wipeOfflineDocuments, wipeOfflineMail, withRelativePath, workflowStepOf, workspaceHouseTemplateId };
|
|
15036
16602
|
//# sourceMappingURL=index.js.map
|
|
15037
16603
|
//# sourceMappingURL=index.js.map
|