@opengeni/react-native 1.4.5-canary.37615713321001
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/LICENSE +190 -0
- package/README.md +74 -0
- package/dist/adapters.d.ts +53 -0
- package/dist/adapters.js +3 -0
- package/dist/adapters.js.map +1 -0
- package/dist/attachments.d.ts +75 -0
- package/dist/chunk-3VSZAQ7G.js +391 -0
- package/dist/chunk-3VSZAQ7G.js.map +1 -0
- package/dist/chunk-5ZVK7WKA.js +88 -0
- package/dist/chunk-5ZVK7WKA.js.map +1 -0
- package/dist/chunk-EEDIMY3L.js +240 -0
- package/dist/chunk-EEDIMY3L.js.map +1 -0
- package/dist/chunk-M23SDI4R.js +83 -0
- package/dist/chunk-M23SDI4R.js.map +1 -0
- package/dist/chunk-PHJBSAR3.js +2006 -0
- package/dist/chunk-PHJBSAR3.js.map +1 -0
- package/dist/environment.d.ts +19 -0
- package/dist/expo.d.ts +25 -0
- package/dist/expo.js +267 -0
- package/dist/expo.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3345 -0
- package/dist/index.js.map +1 -0
- package/dist/presentation.d.ts +262 -0
- package/dist/read-acknowledgement.d.ts +39 -0
- package/dist/realtime/call-events.d.ts +11 -0
- package/dist/realtime/call-view.d.ts +26 -0
- package/dist/realtime/expo-call.d.ts +11 -0
- package/dist/realtime/native-call.d.ts +104 -0
- package/dist/realtime/native-realtime.d.ts +35 -0
- package/dist/realtime/use-native-session-realtime.d.ts +25 -0
- package/dist/session-view.d.ts +87 -0
- package/dist/timeline/activity.d.ts +34 -0
- package/dist/timeline/composer.d.ts +86 -0
- package/dist/timeline/controls.d.ts +27 -0
- package/dist/timeline/decisions.d.ts +29 -0
- package/dist/timeline/feedback.d.ts +26 -0
- package/dist/timeline/icon-nodes.generated.d.ts +738 -0
- package/dist/timeline/icon.d.ts +11 -0
- package/dist/timeline/index.d.ts +32 -0
- package/dist/timeline/index.js +5419 -0
- package/dist/timeline/index.js.map +1 -0
- package/dist/timeline/markdown.d.ts +11 -0
- package/dist/timeline/markdown.js +415 -0
- package/dist/timeline/markdown.js.map +1 -0
- package/dist/timeline/message-attachments.d.ts +13 -0
- package/dist/timeline/message-timeline.d.ts +36 -0
- package/dist/timeline/model-picker.d.ts +17 -0
- package/dist/timeline/primitives.d.ts +85 -0
- package/dist/timeline/queue-dock.d.ts +19 -0
- package/dist/timeline/session-actions.d.ts +34 -0
- package/dist/timeline/session-list.d.ts +50 -0
- package/dist/timeline/session-screen.d.ts +35 -0
- package/dist/timeline/session-signals.d.ts +37 -0
- package/dist/timeline/sheet.d.ts +14 -0
- package/dist/timeline/theme.d.ts +58 -0
- package/dist/timeline/thinking-orb.d.ts +13 -0
- package/dist/timeline/turn.d.ts +80 -0
- package/dist/ui/attention.d.ts +14 -0
- package/dist/ui/cards.d.ts +21 -0
- package/dist/ui/composer.d.ts +21 -0
- package/dist/ui/format.d.ts +13 -0
- package/dist/ui/icons.d.ts +9 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +1757 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/steps-sheet.d.ts +10 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/timeline.d.ts +48 -0
- package/dist/ui/web-tokens.gen.d.ts +90 -0
- package/dist/use-native-session.d.ts +40 -0
- package/dist/view-model.d.ts +52 -0
- package/dist/voice-input.d.ts +102 -0
- package/dist/webrtc.d.ts +9 -0
- package/dist/webrtc.js +19 -0
- package/dist/webrtc.js.map +1 -0
- package/expo-module.config.json +7 -0
- package/ios/OpenGeniCall.podspec +22 -0
- package/ios/OpenGeniCallAppDelegateSubscriber.swift +29 -0
- package/ios/OpenGeniCallCenter.swift +189 -0
- package/ios/OpenGeniCallModule.swift +64 -0
- package/package.json +93 -0
- package/src/adapters.ts +150 -0
- package/src/attachments.ts +396 -0
- package/src/environment.tsx +214 -0
- package/src/expo.ts +295 -0
- package/src/index.ts +98 -0
- package/src/presentation.ts +285 -0
- package/src/read-acknowledgement.ts +72 -0
- package/src/realtime/call-events.ts +41 -0
- package/src/realtime/call-view.tsx +220 -0
- package/src/realtime/expo-call.ts +49 -0
- package/src/realtime/native-call.ts +266 -0
- package/src/realtime/native-realtime.ts +115 -0
- package/src/realtime/use-native-session-realtime.ts +29 -0
- package/src/session-view.tsx +2251 -0
- package/src/timeline/activity.tsx +736 -0
- package/src/timeline/composer.tsx +686 -0
- package/src/timeline/controls.tsx +151 -0
- package/src/timeline/decisions.tsx +651 -0
- package/src/timeline/feedback.tsx +251 -0
- package/src/timeline/icon-nodes.generated.ts +1830 -0
- package/src/timeline/icon.tsx +44 -0
- package/src/timeline/index.ts +67 -0
- package/src/timeline/markdown.tsx +510 -0
- package/src/timeline/message-attachments.tsx +155 -0
- package/src/timeline/message-timeline.tsx +1042 -0
- package/src/timeline/model-picker.tsx +450 -0
- package/src/timeline/primitives.tsx +561 -0
- package/src/timeline/queue-dock.tsx +331 -0
- package/src/timeline/session-actions.tsx +379 -0
- package/src/timeline/session-list.tsx +338 -0
- package/src/timeline/session-screen.tsx +488 -0
- package/src/timeline/session-signals.tsx +312 -0
- package/src/timeline/sheet.tsx +153 -0
- package/src/timeline/theme.tsx +128 -0
- package/src/timeline/thinking-orb.tsx +93 -0
- package/src/timeline/turn.tsx +569 -0
- package/src/ui/attention.tsx +220 -0
- package/src/ui/cards.tsx +267 -0
- package/src/ui/composer.tsx +296 -0
- package/src/ui/format.ts +86 -0
- package/src/ui/icons.tsx +131 -0
- package/src/ui/index.ts +18 -0
- package/src/ui/steps-sheet.tsx +71 -0
- package/src/ui/theme.ts +261 -0
- package/src/ui/timeline.tsx +837 -0
- package/src/ui/web-tokens.gen.ts +94 -0
- package/src/use-native-session.ts +219 -0
- package/src/view-model.ts +485 -0
- package/src/voice-input.ts +382 -0
- package/src/webrtc.ts +30 -0
package/src/adapters.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { FetchLike } from "@opengeni/sdk";
|
|
2
|
+
|
|
3
|
+
export type NativeLifecycleState = "active" | "background" | "inactive";
|
|
4
|
+
|
|
5
|
+
export interface NativeLifecycleAdapter {
|
|
6
|
+
currentState(): NativeLifecycleState;
|
|
7
|
+
subscribe(listener: (state: NativeLifecycleState) => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NativePersistenceAdapter {
|
|
11
|
+
hydrate(): Promise<void>;
|
|
12
|
+
getItem(key: string): string | null;
|
|
13
|
+
setItem(key: string, value: string): void;
|
|
14
|
+
removeItem(key: string): void;
|
|
15
|
+
clear(): void;
|
|
16
|
+
flush(): Promise<void>;
|
|
17
|
+
invalidate(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface AsyncKeyValueStorage {
|
|
21
|
+
getAllKeys(): Promise<readonly string[]>;
|
|
22
|
+
multiGet(keys: readonly string[]): Promise<readonly (readonly [string, string | null])[]>;
|
|
23
|
+
setItem(key: string, value: string): Promise<void>;
|
|
24
|
+
removeItem(key: string): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface NativeCryptoAdapter {
|
|
28
|
+
randomUUID(): string;
|
|
29
|
+
sha256Hex(bytes: Uint8Array): Promise<string>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface NativePickedFile {
|
|
33
|
+
id: string;
|
|
34
|
+
uri: string;
|
|
35
|
+
name: string;
|
|
36
|
+
contentType: string;
|
|
37
|
+
sizeBytes: number | null;
|
|
38
|
+
kind: "document" | "image";
|
|
39
|
+
previewUri?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NativeFileAdapter {
|
|
43
|
+
pickDocuments(): Promise<NativePickedFile[]>;
|
|
44
|
+
pickImages(): Promise<NativePickedFile[]>;
|
|
45
|
+
readBytes(file: NativePickedFile): Promise<Uint8Array>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface OpenGeniReactNativeAdapters {
|
|
49
|
+
lifecycle: NativeLifecycleAdapter;
|
|
50
|
+
persistence: NativePersistenceAdapter;
|
|
51
|
+
crypto: NativeCryptoAdapter;
|
|
52
|
+
files: NativeFileAdapter;
|
|
53
|
+
fetch: FetchLike;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build the synchronous storage facade required by the official composer from
|
|
58
|
+
* an asynchronous native key/value store. Rendering waits for `hydrate`, so
|
|
59
|
+
* reads never race the initial native load.
|
|
60
|
+
*/
|
|
61
|
+
export function createHydratedPersistenceAdapter(
|
|
62
|
+
storage: AsyncKeyValueStorage,
|
|
63
|
+
namespace: string,
|
|
64
|
+
): NativePersistenceAdapter {
|
|
65
|
+
const prefix = `${namespace.replace(/:+$/, "")}:`;
|
|
66
|
+
const memory = new Map<string, string>();
|
|
67
|
+
let hydrated = false;
|
|
68
|
+
let hydration: Promise<void> | null = null;
|
|
69
|
+
let active = true;
|
|
70
|
+
let mutations = Promise.resolve();
|
|
71
|
+
let invalidation: Promise<void> | null = null;
|
|
72
|
+
|
|
73
|
+
const enqueueMutation = (operation: () => Promise<void>) => {
|
|
74
|
+
mutations = mutations.then(operation, operation).catch(() => undefined);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const removePersistedNamespace = async () => {
|
|
78
|
+
const keys = (await storage.getAllKeys()).filter((key) => key.startsWith(prefix));
|
|
79
|
+
await Promise.all(keys.map((key) => storage.removeItem(key)));
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
hydrate: async () => {
|
|
84
|
+
if (!active) return;
|
|
85
|
+
hydration ??= (async () => {
|
|
86
|
+
const keys = (await storage.getAllKeys()).filter((key) => key.startsWith(prefix));
|
|
87
|
+
const entries = keys.length > 0 ? await storage.multiGet(keys) : [];
|
|
88
|
+
if (!active) return;
|
|
89
|
+
memory.clear();
|
|
90
|
+
for (const [key, value] of entries) {
|
|
91
|
+
if (value !== null) memory.set(key.slice(prefix.length), value);
|
|
92
|
+
}
|
|
93
|
+
hydrated = true;
|
|
94
|
+
})();
|
|
95
|
+
await hydration;
|
|
96
|
+
},
|
|
97
|
+
getItem: (key) => {
|
|
98
|
+
if (!active || !hydrated) return null;
|
|
99
|
+
return memory.get(key) ?? null;
|
|
100
|
+
},
|
|
101
|
+
setItem: (key, value) => {
|
|
102
|
+
if (!active) return;
|
|
103
|
+
memory.set(key, value);
|
|
104
|
+
enqueueMutation(async () => {
|
|
105
|
+
if (!active) return;
|
|
106
|
+
await storage.setItem(`${prefix}${key}`, value);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
removeItem: (key) => {
|
|
110
|
+
if (!active) return;
|
|
111
|
+
memory.delete(key);
|
|
112
|
+
enqueueMutation(async () => {
|
|
113
|
+
if (!active) return;
|
|
114
|
+
await storage.removeItem(`${prefix}${key}`);
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
clear: () => {
|
|
118
|
+
if (!active) return;
|
|
119
|
+
memory.clear();
|
|
120
|
+
enqueueMutation(async () => {
|
|
121
|
+
if (!active) return;
|
|
122
|
+
await removePersistedNamespace();
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
flush: async () => await mutations,
|
|
126
|
+
invalidate: () => {
|
|
127
|
+
if (invalidation) return invalidation;
|
|
128
|
+
active = false;
|
|
129
|
+
hydrated = false;
|
|
130
|
+
memory.clear();
|
|
131
|
+
invalidation = (async () => {
|
|
132
|
+
await hydration?.catch(() => undefined);
|
|
133
|
+
await mutations;
|
|
134
|
+
await removePersistedNamespace();
|
|
135
|
+
})();
|
|
136
|
+
return invalidation;
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function sha256HexToArrayBuffer(hex: string): ArrayBuffer {
|
|
142
|
+
if (!/^[a-f\d]{64}$/i.test(hex)) {
|
|
143
|
+
throw new TypeError("SHA-256 adapter returned an invalid digest");
|
|
144
|
+
}
|
|
145
|
+
const bytes = new Uint8Array(32);
|
|
146
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
147
|
+
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
148
|
+
}
|
|
149
|
+
return bytes.buffer;
|
|
150
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import type { FileAsset, ResourceRef } from "@opengeni/sdk";
|
|
3
|
+
import type { FileAttachmentClientLike } from "@opengeni/react/session";
|
|
4
|
+
import type { NativeCryptoAdapter, NativeFileAdapter, NativePickedFile } from "./adapters";
|
|
5
|
+
|
|
6
|
+
export type NativeAttachmentStatus = "preparing" | "uploading" | "ready" | "failed";
|
|
7
|
+
|
|
8
|
+
export const NATIVE_ATTACHMENT_PREPARATION_TIMEOUT_MS = 30_000;
|
|
9
|
+
|
|
10
|
+
export interface NativeAttachment {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
contentType: string;
|
|
14
|
+
sizeBytes: number | null;
|
|
15
|
+
kind: NativePickedFile["kind"];
|
|
16
|
+
previewUri?: string | undefined;
|
|
17
|
+
status: NativeAttachmentStatus;
|
|
18
|
+
file?: FileAsset | undefined;
|
|
19
|
+
error?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface NativeFileAttachmentsResult {
|
|
23
|
+
attachments: NativeAttachment[];
|
|
24
|
+
readyResources: ResourceRef[];
|
|
25
|
+
uploading: boolean;
|
|
26
|
+
hasUnresolved: boolean;
|
|
27
|
+
error: Error | null;
|
|
28
|
+
pickDocuments(): Promise<void>;
|
|
29
|
+
pickImages(): Promise<void>;
|
|
30
|
+
addFiles(files: NativePickedFile[]): Promise<void>;
|
|
31
|
+
retry(id: string): Promise<void>;
|
|
32
|
+
remove(id: string): void;
|
|
33
|
+
clear(): void;
|
|
34
|
+
clearError(): void;
|
|
35
|
+
/** Show already-uploaded files (a restored draft) as ready attachments. */
|
|
36
|
+
restoreReady(files: Iterable<FileAsset>): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface NativeAttachmentScope {
|
|
40
|
+
client: FileAttachmentClientLike;
|
|
41
|
+
workspaceId: string;
|
|
42
|
+
sessionId: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function sameNativeAttachmentScope(
|
|
46
|
+
left: NativeAttachmentScope,
|
|
47
|
+
right: NativeAttachmentScope,
|
|
48
|
+
): boolean {
|
|
49
|
+
return (
|
|
50
|
+
left.client === right.client &&
|
|
51
|
+
left.workspaceId === right.workspaceId &&
|
|
52
|
+
left.sessionId === right.sessionId
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function projectNativeAttachmentScopeState(
|
|
57
|
+
scopeMatches: boolean,
|
|
58
|
+
attachments: NativeAttachment[],
|
|
59
|
+
error: Error | null,
|
|
60
|
+
): { attachments: NativeAttachment[]; error: Error | null } {
|
|
61
|
+
return scopeMatches ? { attachments, error } : { attachments: [], error: null };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runNativeAttachmentPicker(input: {
|
|
65
|
+
generation: number;
|
|
66
|
+
currentGeneration(): number;
|
|
67
|
+
pick(): Promise<NativePickedFile[]>;
|
|
68
|
+
addFiles(files: NativePickedFile[], generation: number): Promise<void>;
|
|
69
|
+
onError(error: Error): void;
|
|
70
|
+
}): Promise<"added" | "failed" | "stale"> {
|
|
71
|
+
try {
|
|
72
|
+
const files = await input.pick();
|
|
73
|
+
if (input.currentGeneration() !== input.generation) return "stale";
|
|
74
|
+
await input.addFiles(files, input.generation);
|
|
75
|
+
return input.currentGeneration() === input.generation ? "added" : "stale";
|
|
76
|
+
} catch (cause) {
|
|
77
|
+
if (input.currentGeneration() !== input.generation) return "stale";
|
|
78
|
+
input.onError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
79
|
+
return "failed";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function addNativeFilesForGeneration(input: {
|
|
84
|
+
files: NativePickedFile[];
|
|
85
|
+
generation: number;
|
|
86
|
+
currentGeneration(): number;
|
|
87
|
+
hasSource(id: string): boolean;
|
|
88
|
+
rememberSource(file: NativePickedFile): void;
|
|
89
|
+
appendAttachments(files: NativePickedFile[]): void;
|
|
90
|
+
upload(file: NativePickedFile, generation: number): Promise<void>;
|
|
91
|
+
}): Promise<void> {
|
|
92
|
+
if (input.currentGeneration() !== input.generation) return;
|
|
93
|
+
const unique = input.files.filter((file) => !input.hasSource(file.id));
|
|
94
|
+
if (unique.length === 0) return;
|
|
95
|
+
for (const file of unique) input.rememberSource(file);
|
|
96
|
+
input.appendAttachments(unique);
|
|
97
|
+
await unique.reduce(async (previous, file) => {
|
|
98
|
+
await previous;
|
|
99
|
+
if (input.currentGeneration() !== input.generation) return;
|
|
100
|
+
await input.upload(file, input.generation);
|
|
101
|
+
}, Promise.resolve());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function prepareNativeAttachmentUpload(input: {
|
|
105
|
+
source: NativePickedFile;
|
|
106
|
+
files: NativeFileAdapter;
|
|
107
|
+
crypto: NativeCryptoAdapter;
|
|
108
|
+
timeoutMs?: number | undefined;
|
|
109
|
+
}): Promise<{ bytes: Uint8Array; data: ArrayBuffer; sha256: string }> {
|
|
110
|
+
const timeoutMs = input.timeoutMs ?? NATIVE_ATTACHMENT_PREPARATION_TIMEOUT_MS;
|
|
111
|
+
const bytes = await withTimeout(
|
|
112
|
+
input.files.readBytes(input.source),
|
|
113
|
+
timeoutMs,
|
|
114
|
+
"File preparation timed out. Retry the upload.",
|
|
115
|
+
);
|
|
116
|
+
const sha256 = await withTimeout(
|
|
117
|
+
input.crypto.sha256Hex(bytes),
|
|
118
|
+
timeoutMs,
|
|
119
|
+
"File verification timed out. Retry the upload.",
|
|
120
|
+
);
|
|
121
|
+
// The OpenGeni SDK accepts either Uint8Array or ArrayBuffer, but its
|
|
122
|
+
// Uint8Array snapshot uses `new Blob([view])`. React Native's Blob rejects
|
|
123
|
+
// ArrayBufferView inputs, so pass an exact ArrayBuffer and avoid that branch.
|
|
124
|
+
const uploadBytes = Uint8Array.from(bytes);
|
|
125
|
+
const data = uploadBytes.buffer;
|
|
126
|
+
return { bytes, data, sha256 };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function withTimeout<T>(
|
|
130
|
+
operation: Promise<T>,
|
|
131
|
+
timeoutMs: number,
|
|
132
|
+
message: string,
|
|
133
|
+
): Promise<T> {
|
|
134
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
135
|
+
throw new Error("Native attachment timeout must be a positive number");
|
|
136
|
+
}
|
|
137
|
+
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
138
|
+
const timedOut = new Promise<never>((_resolve, reject) => {
|
|
139
|
+
timeout = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
140
|
+
});
|
|
141
|
+
try {
|
|
142
|
+
return await Promise.race([operation, timedOut]);
|
|
143
|
+
} finally {
|
|
144
|
+
if (timeout !== undefined) clearTimeout(timeout);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function useNativeFileAttachments(input: {
|
|
149
|
+
client: FileAttachmentClientLike;
|
|
150
|
+
workspaceId: string;
|
|
151
|
+
sessionId: string;
|
|
152
|
+
files: NativeFileAdapter;
|
|
153
|
+
crypto: NativeCryptoAdapter;
|
|
154
|
+
}): NativeFileAttachmentsResult {
|
|
155
|
+
const [attachments, setAttachments] = useState<NativeAttachment[]>([]);
|
|
156
|
+
const [error, setError] = useState<Error | null>(null);
|
|
157
|
+
const sources = useRef(new Map<string, NativePickedFile>());
|
|
158
|
+
const attempts = useRef(new Map<string, number>());
|
|
159
|
+
const scopeGeneration = useRef(0);
|
|
160
|
+
const currentScope = {
|
|
161
|
+
client: input.client,
|
|
162
|
+
workspaceId: input.workspaceId,
|
|
163
|
+
sessionId: input.sessionId,
|
|
164
|
+
} satisfies NativeAttachmentScope;
|
|
165
|
+
const previousScope = useRef(currentScope);
|
|
166
|
+
const scopeMatches = sameNativeAttachmentScope(previousScope.current, currentScope);
|
|
167
|
+
|
|
168
|
+
useLayoutEffect(() => {
|
|
169
|
+
const nextScope = {
|
|
170
|
+
client: input.client,
|
|
171
|
+
workspaceId: input.workspaceId,
|
|
172
|
+
sessionId: input.sessionId,
|
|
173
|
+
};
|
|
174
|
+
if (sameNativeAttachmentScope(previousScope.current, nextScope)) return;
|
|
175
|
+
previousScope.current = nextScope;
|
|
176
|
+
scopeGeneration.current += 1;
|
|
177
|
+
sources.current.clear();
|
|
178
|
+
attempts.current.clear();
|
|
179
|
+
setAttachments([]);
|
|
180
|
+
setError(null);
|
|
181
|
+
}, [input.client, input.sessionId, input.workspaceId]);
|
|
182
|
+
|
|
183
|
+
useEffect(
|
|
184
|
+
() => () => {
|
|
185
|
+
scopeGeneration.current += 1;
|
|
186
|
+
sources.current.clear();
|
|
187
|
+
attempts.current.clear();
|
|
188
|
+
},
|
|
189
|
+
[],
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const upload = useCallback(
|
|
193
|
+
async (source: NativePickedFile, generation = scopeGeneration.current) => {
|
|
194
|
+
if (scopeGeneration.current !== generation) return;
|
|
195
|
+
const attempt = (attempts.current.get(source.id) ?? 0) + 1;
|
|
196
|
+
attempts.current.set(source.id, attempt);
|
|
197
|
+
setAttachments((current) =>
|
|
198
|
+
current.map((entry) =>
|
|
199
|
+
entry.id === source.id ? { ...entry, status: "preparing", error: undefined } : entry,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
try {
|
|
203
|
+
const { data, sha256 } = await prepareNativeAttachmentUpload({
|
|
204
|
+
source,
|
|
205
|
+
files: input.files,
|
|
206
|
+
crypto: input.crypto,
|
|
207
|
+
});
|
|
208
|
+
if (
|
|
209
|
+
scopeGeneration.current !== generation ||
|
|
210
|
+
attempts.current.get(source.id) !== attempt ||
|
|
211
|
+
!sources.current.has(source.id)
|
|
212
|
+
) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
setAttachments((current) =>
|
|
216
|
+
current.map((entry) =>
|
|
217
|
+
entry.id === source.id ? { ...entry, status: "uploading" } : entry,
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
const file = await input.client.uploadFile(input.workspaceId, {
|
|
221
|
+
filename: source.name,
|
|
222
|
+
contentType: source.contentType,
|
|
223
|
+
data,
|
|
224
|
+
sha256,
|
|
225
|
+
});
|
|
226
|
+
if (
|
|
227
|
+
scopeGeneration.current !== generation ||
|
|
228
|
+
attempts.current.get(source.id) !== attempt ||
|
|
229
|
+
!sources.current.has(source.id)
|
|
230
|
+
) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
sources.current.delete(source.id);
|
|
234
|
+
attempts.current.delete(source.id);
|
|
235
|
+
setAttachments((current) =>
|
|
236
|
+
current.map((entry) =>
|
|
237
|
+
entry.id === source.id ? { ...entry, status: "ready", file, error: undefined } : entry,
|
|
238
|
+
),
|
|
239
|
+
);
|
|
240
|
+
} catch (cause) {
|
|
241
|
+
if (
|
|
242
|
+
scopeGeneration.current !== generation ||
|
|
243
|
+
attempts.current.get(source.id) !== attempt ||
|
|
244
|
+
!sources.current.has(source.id)
|
|
245
|
+
) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
setAttachments((current) =>
|
|
249
|
+
current.map((entry) =>
|
|
250
|
+
entry.id === source.id
|
|
251
|
+
? {
|
|
252
|
+
...entry,
|
|
253
|
+
status: "failed",
|
|
254
|
+
error: cause instanceof Error ? cause.message : String(cause),
|
|
255
|
+
}
|
|
256
|
+
: entry,
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
[input.client, input.crypto, input.files, input.workspaceId],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const addFilesForGeneration = useCallback(
|
|
265
|
+
async (files: NativePickedFile[], generation: number) => {
|
|
266
|
+
await addNativeFilesForGeneration({
|
|
267
|
+
files,
|
|
268
|
+
generation,
|
|
269
|
+
currentGeneration: () => scopeGeneration.current,
|
|
270
|
+
hasSource: (id) => sources.current.has(id),
|
|
271
|
+
rememberSource: (file) => sources.current.set(file.id, file),
|
|
272
|
+
appendAttachments: (unique) =>
|
|
273
|
+
setAttachments((current) => [
|
|
274
|
+
...current,
|
|
275
|
+
...unique.map(
|
|
276
|
+
(file): NativeAttachment => ({
|
|
277
|
+
id: file.id,
|
|
278
|
+
name: file.name,
|
|
279
|
+
contentType: file.contentType,
|
|
280
|
+
sizeBytes: file.sizeBytes,
|
|
281
|
+
kind: file.kind,
|
|
282
|
+
...(file.previewUri ? { previewUri: file.previewUri } : {}),
|
|
283
|
+
status: "preparing",
|
|
284
|
+
}),
|
|
285
|
+
),
|
|
286
|
+
]),
|
|
287
|
+
upload,
|
|
288
|
+
});
|
|
289
|
+
},
|
|
290
|
+
[upload],
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const addFiles = useCallback(
|
|
294
|
+
async (files: NativePickedFile[]) =>
|
|
295
|
+
await addFilesForGeneration(files, scopeGeneration.current),
|
|
296
|
+
[addFilesForGeneration],
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const pickFiles = useCallback(
|
|
300
|
+
async (pick: () => Promise<NativePickedFile[]>) => {
|
|
301
|
+
const generation = scopeGeneration.current;
|
|
302
|
+
setError(null);
|
|
303
|
+
await runNativeAttachmentPicker({
|
|
304
|
+
generation,
|
|
305
|
+
currentGeneration: () => scopeGeneration.current,
|
|
306
|
+
pick,
|
|
307
|
+
addFiles: addFilesForGeneration,
|
|
308
|
+
onError: setError,
|
|
309
|
+
});
|
|
310
|
+
},
|
|
311
|
+
[addFilesForGeneration],
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const pickDocuments = useCallback(
|
|
315
|
+
async () => await pickFiles(() => input.files.pickDocuments()),
|
|
316
|
+
[input.files, pickFiles],
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const pickImages = useCallback(
|
|
320
|
+
async () => await pickFiles(() => input.files.pickImages()),
|
|
321
|
+
[input.files, pickFiles],
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const retry = useCallback(
|
|
325
|
+
async (id: string) => {
|
|
326
|
+
const source = sources.current.get(id);
|
|
327
|
+
if (source) await upload(source);
|
|
328
|
+
},
|
|
329
|
+
[upload],
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const remove = useCallback((id: string) => {
|
|
333
|
+
sources.current.delete(id);
|
|
334
|
+
attempts.current.delete(id);
|
|
335
|
+
setAttachments((current) => current.filter((entry) => entry.id !== id));
|
|
336
|
+
}, []);
|
|
337
|
+
|
|
338
|
+
const clear = useCallback(() => {
|
|
339
|
+
sources.current.clear();
|
|
340
|
+
attempts.current.clear();
|
|
341
|
+
setAttachments([]);
|
|
342
|
+
setError(null);
|
|
343
|
+
}, []);
|
|
344
|
+
const clearError = useCallback(() => setError(null), []);
|
|
345
|
+
const restoreReady = useCallback((files: Iterable<FileAsset>) => {
|
|
346
|
+
const restored = [...files];
|
|
347
|
+
setAttachments((current) => {
|
|
348
|
+
const kept = current.filter((entry) => entry.status !== "ready");
|
|
349
|
+
return [
|
|
350
|
+
...restored.map(
|
|
351
|
+
(file): NativeAttachment => ({
|
|
352
|
+
id: `restored:${file.id}`,
|
|
353
|
+
name: file.filename,
|
|
354
|
+
contentType: file.contentType,
|
|
355
|
+
sizeBytes: file.sizeBytes,
|
|
356
|
+
kind: file.contentType.startsWith("image/") ? "image" : "document",
|
|
357
|
+
status: "ready",
|
|
358
|
+
file,
|
|
359
|
+
}),
|
|
360
|
+
),
|
|
361
|
+
...kept,
|
|
362
|
+
];
|
|
363
|
+
});
|
|
364
|
+
}, []);
|
|
365
|
+
|
|
366
|
+
const visibleState = projectNativeAttachmentScopeState(scopeMatches, attachments, error);
|
|
367
|
+
const visibleAttachments = visibleState.attachments;
|
|
368
|
+
const visibleError = visibleState.error;
|
|
369
|
+
const readyResources = useMemo<ResourceRef[]>(
|
|
370
|
+
() =>
|
|
371
|
+
visibleAttachments.flatMap((attachment) =>
|
|
372
|
+
attachment.status === "ready" && attachment.file
|
|
373
|
+
? [{ kind: "file" as const, fileId: attachment.file.id }]
|
|
374
|
+
: [],
|
|
375
|
+
),
|
|
376
|
+
[visibleAttachments],
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
attachments: visibleAttachments,
|
|
381
|
+
readyResources,
|
|
382
|
+
uploading: visibleAttachments.some(
|
|
383
|
+
(attachment) => attachment.status === "preparing" || attachment.status === "uploading",
|
|
384
|
+
),
|
|
385
|
+
hasUnresolved: visibleAttachments.some((attachment) => attachment.status !== "ready"),
|
|
386
|
+
error: visibleError,
|
|
387
|
+
pickDocuments,
|
|
388
|
+
pickImages,
|
|
389
|
+
addFiles,
|
|
390
|
+
retry,
|
|
391
|
+
remove,
|
|
392
|
+
clear,
|
|
393
|
+
clearError,
|
|
394
|
+
restoreReady,
|
|
395
|
+
};
|
|
396
|
+
}
|