@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
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { HumanInputAnswer, HumanInputQuestion, SessionStatus } from "@opengeni/sdk";
|
|
2
|
+
import type { ActivityItem, TimelineItem } from "@opengeni/react/session";
|
|
3
|
+
import { type OpenGeniNativeLabels } from "./presentation.js";
|
|
4
|
+
export declare function nativeKnowledgeOutcomeLabel(item: Extract<TimelineItem, {
|
|
5
|
+
kind: "knowledge";
|
|
6
|
+
}>, labels: OpenGeniNativeLabels): string;
|
|
7
|
+
export type NativeSessionStatusTone = "neutral" | "working" | "attention" | "failed" | "complete";
|
|
8
|
+
export interface NativeActivityPresentation {
|
|
9
|
+
title: string;
|
|
10
|
+
preview: string;
|
|
11
|
+
status: string | null;
|
|
12
|
+
detail: string | null;
|
|
13
|
+
expandedByDefault: boolean;
|
|
14
|
+
tone: "neutral" | "running" | "failed";
|
|
15
|
+
}
|
|
16
|
+
export declare function nativeSessionStatusTone(status: SessionStatus | null): NativeSessionStatusTone;
|
|
17
|
+
export declare function boundedJson(value: unknown, maximum?: number): string;
|
|
18
|
+
export declare function inspectableValuePreview(value: unknown): string;
|
|
19
|
+
export declare function nativeToolDisplayName(name: string): string;
|
|
20
|
+
export declare function nativeActivityPresentation(item: ActivityItem, labels: OpenGeniNativeLabels): NativeActivityPresentation;
|
|
21
|
+
export declare function formatNativeRelativeTime(iso: string, labels: Pick<OpenGeniNativeLabels, "relativeNow" | "relativeSeconds" | "relativeMinutes" | "relativeHours" | "relativeDays">, now?: Date): string;
|
|
22
|
+
export declare function nativeTimelineStatusLabel(status: "running" | "complete" | "failed" | "cancelled", labels: OpenGeniNativeLabels): string;
|
|
23
|
+
export declare function nativeGoalActionLabel(action: Extract<TimelineItem, {
|
|
24
|
+
kind: "goal";
|
|
25
|
+
}>["action"], labels: OpenGeniNativeLabels): string;
|
|
26
|
+
export declare function nativeHumanInputRequestPreview(requests: readonly {
|
|
27
|
+
questions: readonly Pick<HumanInputQuestion, "label" | "prompt">[];
|
|
28
|
+
}[]): string;
|
|
29
|
+
export declare function nativeHumanInputSummary(item: Extract<TimelineItem, {
|
|
30
|
+
kind: "human-input";
|
|
31
|
+
}>, labels: OpenGeniNativeLabels): string;
|
|
32
|
+
export declare function nativeContextCompactionPhaseLabel(phase: Extract<TimelineItem, {
|
|
33
|
+
kind: "context-compaction";
|
|
34
|
+
}>["phase"], labels: OpenGeniNativeLabels): string;
|
|
35
|
+
export declare function nativeMemoryActionLabel(item: Extract<TimelineItem, {
|
|
36
|
+
kind: "memory";
|
|
37
|
+
}>, labels: OpenGeniNativeLabels): string;
|
|
38
|
+
export declare function nativeFleetDecisionSummary(item: Extract<TimelineItem, {
|
|
39
|
+
kind: "fleet-decision";
|
|
40
|
+
}>, labels: OpenGeniNativeLabels): string;
|
|
41
|
+
export declare function timelineAccessibilityLabel(item: TimelineItem, labels: OpenGeniNativeLabels): string;
|
|
42
|
+
export declare function nativeSessionStatusLabel(status: SessionStatus, labels: OpenGeniNativeLabels): string;
|
|
43
|
+
export declare function validateHumanInputAnswers(questions: readonly HumanInputQuestion[], drafts: Readonly<Record<string, NativeHumanInputDraft>>): {
|
|
44
|
+
answers: HumanInputAnswer[];
|
|
45
|
+
valid: boolean;
|
|
46
|
+
};
|
|
47
|
+
export interface NativeHumanInputDraft {
|
|
48
|
+
values: string[];
|
|
49
|
+
other: string;
|
|
50
|
+
otherSelected: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare function emptyHumanInputDraft(): NativeHumanInputDraft;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** A finished recording on the device. */
|
|
2
|
+
export interface NativeVoiceRecording {
|
|
3
|
+
uri: string;
|
|
4
|
+
mimeType: string;
|
|
5
|
+
durationSeconds: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The platform recorder behind dictation. The Expo implementation lives in
|
|
9
|
+
* `@opengeni/react-native/expo` (`useExpoVoiceRecorder`).
|
|
10
|
+
*/
|
|
11
|
+
export interface NativeVoiceRecorder {
|
|
12
|
+
/** Ask for microphone access; resolves false when the person declines. */
|
|
13
|
+
requestPermission(): Promise<boolean>;
|
|
14
|
+
start(): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Stop and keep the recording; null when nothing was captured. The file must
|
|
17
|
+
* survive a following `start()`: dictation keeps it until its text is in the
|
|
18
|
+
* draft, then calls `discard`.
|
|
19
|
+
*/
|
|
20
|
+
stop(): Promise<NativeVoiceRecording | null>;
|
|
21
|
+
/** Stop and throw the current recording away. */
|
|
22
|
+
cancel(): Promise<void>;
|
|
23
|
+
/** Delete a kept recording file once it is no longer needed. */
|
|
24
|
+
discard?(recording: NativeVoiceRecording): void;
|
|
25
|
+
/** Seconds captured so far while recording. */
|
|
26
|
+
durationSeconds(): number;
|
|
27
|
+
/** The current input level from 0 to 1 while recording, when the platform meters it. */
|
|
28
|
+
level(): number | null;
|
|
29
|
+
}
|
|
30
|
+
export type NativeVoiceInputStatus = "idle" | "requesting-permission" | "recording" | "transcribing" | "error";
|
|
31
|
+
export interface NativeVoiceInput {
|
|
32
|
+
/** Dictation is offered: a recorder exists and the deployment can transcribe. */
|
|
33
|
+
available: boolean;
|
|
34
|
+
status: NativeVoiceInputStatus;
|
|
35
|
+
error: string | null;
|
|
36
|
+
/** Total seconds captured in this dictation, across segments. */
|
|
37
|
+
durationSeconds: number;
|
|
38
|
+
/** Input level from 0 to 1 while recording (null when not metered). */
|
|
39
|
+
level: number | null;
|
|
40
|
+
/**
|
|
41
|
+
* Always null: long dictation rolls over into segments under the server's
|
|
42
|
+
* per-request limit, so there is no recording cap to count down to.
|
|
43
|
+
*/
|
|
44
|
+
maxDurationSeconds: number | null;
|
|
45
|
+
/** A finished recording is kept on the device after a failed transcription. */
|
|
46
|
+
hasSavedRecording: boolean;
|
|
47
|
+
start(): void;
|
|
48
|
+
/** Stop, transcribe and append the text to the draft. */
|
|
49
|
+
stop(): void;
|
|
50
|
+
/** Throw the current dictation away (recording or saved). */
|
|
51
|
+
cancel(): void;
|
|
52
|
+
/** Transcribe a saved recording again. */
|
|
53
|
+
retry(): void;
|
|
54
|
+
/** Hide the error message; a saved recording stays available to retry. */
|
|
55
|
+
clearError(): void;
|
|
56
|
+
}
|
|
57
|
+
/** Append dictated text to the draft, as the web composer does. */
|
|
58
|
+
export declare function appendDictation(draft: string, transcript: string): string;
|
|
59
|
+
/** An error from the transcription request, with the HTTP status when there was one. */
|
|
60
|
+
export declare class NativeTranscriptionError extends Error {
|
|
61
|
+
readonly status: number | null;
|
|
62
|
+
constructor(message: string, status: number | null);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether trying the same audio again can succeed: network failures, timeouts,
|
|
66
|
+
* rate limits and server errors. Rejections of the audio itself (bad request,
|
|
67
|
+
* too large, unsupported, forbidden) do not change on retry.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isRetryableTranscriptionError(cause: unknown): boolean;
|
|
70
|
+
/** Delays between automatic attempts; the recording is kept whatever happens. */
|
|
71
|
+
export declare const TRANSCRIPTION_RETRY_DELAYS_MS: readonly [1000, 3000, 8000];
|
|
72
|
+
/**
|
|
73
|
+
* Where a long recording rolls over into a new segment: a little under the
|
|
74
|
+
* server's per-request limit, so a segment is never rejected as too long.
|
|
75
|
+
*/
|
|
76
|
+
export declare function dictationSegmentSeconds(maxDurationSeconds: number | null): number | null;
|
|
77
|
+
/**
|
|
78
|
+
* Dictation for a native composer: record on the device, send the audio to the
|
|
79
|
+
* deployment's transcription API, and append the text to the draft. The draft
|
|
80
|
+
* is read at completion, so typing while transcribing is preserved.
|
|
81
|
+
*
|
|
82
|
+
* Nothing said is lost: a long recording rolls over into segments under the
|
|
83
|
+
* per-request limit (transcribed in the background while recording goes on),
|
|
84
|
+
* failed requests are retried, and a recording whose text could not be fetched
|
|
85
|
+
* stays on the device until it is transcribed or the person discards it.
|
|
86
|
+
*/
|
|
87
|
+
export declare function useNativeVoiceInput(input: {
|
|
88
|
+
recorder: NativeVoiceRecorder | null;
|
|
89
|
+
/** Whether the deployment offers transcription in this workspace. */
|
|
90
|
+
available: boolean;
|
|
91
|
+
/** The server's per-request duration limit; long dictation is segmented under it. */
|
|
92
|
+
maxDurationSeconds?: number | null | undefined;
|
|
93
|
+
transcribe: (recording: NativeVoiceRecording) => Promise<string>;
|
|
94
|
+
getValue: () => string;
|
|
95
|
+
setValue: (value: string) => void;
|
|
96
|
+
/** Host feedback (haptics) when recording starts and stops. */
|
|
97
|
+
onFeedback?: ((event: "start" | "stop" | "cancel") => void) | undefined;
|
|
98
|
+
/** Scope key; changing it cancels a running capture (another session or workspace). */
|
|
99
|
+
scope?: string | undefined;
|
|
100
|
+
/** Test seam for the retry delays. */
|
|
101
|
+
retryDelaysMs?: readonly number[] | undefined;
|
|
102
|
+
}): NativeVoiceInput;
|
package/dist/webrtc.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NativeCallAdapter } from "./realtime/native-call.js";
|
|
2
|
+
import type { NativeWebRtcAdapter } from "./realtime/native-realtime.js";
|
|
3
|
+
export declare function createReactNativeWebRtcAdapter(configuration?: RTCConfiguration): NativeWebRtcAdapter;
|
|
4
|
+
/**
|
|
5
|
+
* Hand the system call's audio session to WebRTC: CallKit activates it (so
|
|
6
|
+
* audio keeps running with the phone locked and follows AirPods or CarPlay),
|
|
7
|
+
* and WebRTC starts and stops its voice-processing audio on those signals.
|
|
8
|
+
*/
|
|
9
|
+
export declare function connectCallAudioToWebRtc(call: NativeCallAdapter): () => void;
|
package/dist/webrtc.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RTCPeerConnection, mediaDevices, RTCAudioSession } from 'react-native-webrtc';
|
|
2
|
+
|
|
3
|
+
// src/webrtc.ts
|
|
4
|
+
function createReactNativeWebRtcAdapter(configuration = {}) {
|
|
5
|
+
return {
|
|
6
|
+
createPeerConnection: () => new RTCPeerConnection(configuration),
|
|
7
|
+
getUserMedia: async (constraints) => await mediaDevices.getUserMedia(constraints)
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function connectCallAudioToWebRtc(call) {
|
|
11
|
+
return call.subscribe((event) => {
|
|
12
|
+
if (event.type === "audioSessionActivated") RTCAudioSession.audioSessionDidActivate();
|
|
13
|
+
else if (event.type === "audioSessionDeactivated") RTCAudioSession.audioSessionDidDeactivate();
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { connectCallAudioToWebRtc, createReactNativeWebRtcAdapter };
|
|
18
|
+
//# sourceMappingURL=webrtc.js.map
|
|
19
|
+
//# sourceMappingURL=webrtc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/webrtc.ts"],"names":[],"mappings":";;;AAQO,SAAS,8BAAA,CACd,aAAA,GAAkC,EAAC,EACd;AACrB,EAAA,OAAO;AAAA,IACL,oBAAA,EAAsB,MACpB,IAAI,iBAAA,CAAkB,aAAsB,CAAA;AAAA,IAC9C,cAAc,OAAO,WAAA,KAClB,MAAM,YAAA,CAAa,aAAa,WAAoB;AAAA,GACzD;AACF;AAOO,SAAS,yBAAyB,IAAA,EAAqC;AAC5E,EAAA,OAAO,IAAA,CAAK,SAAA,CAAU,CAAC,KAAA,KAAU;AAC/B,IAAA,IAAI,KAAA,CAAM,IAAA,KAAS,uBAAA,EAAyB,eAAA,CAAgB,uBAAA,EAAwB;AAAA,SAAA,IAC3E,KAAA,CAAM,IAAA,KAAS,yBAAA,EAA2B,eAAA,CAAgB,yBAAA,EAA0B;AAAA,EAC/F,CAAC,CAAA;AACH","file":"webrtc.js","sourcesContent":["// @opengeni/react-native/webrtc: the ready-made native WebRTC adapter.\n//\n// A separate entry point so hosts without voice never load the native WebRTC\n// module. Requires the optional peer dependency `react-native-webrtc`.\nimport { mediaDevices, RTCAudioSession, RTCPeerConnection } from \"react-native-webrtc\";\nimport type { NativeCallAdapter } from \"./realtime/native-call\";\nimport type { NativeWebRtcAdapter } from \"./realtime/native-realtime\";\n\nexport function createReactNativeWebRtcAdapter(\n configuration: RTCConfiguration = {},\n): NativeWebRtcAdapter {\n return {\n createPeerConnection: () =>\n new RTCPeerConnection(configuration as never) as unknown as globalThis.RTCPeerConnection,\n getUserMedia: async (constraints) =>\n (await mediaDevices.getUserMedia(constraints as never)) as unknown as MediaStream,\n };\n}\n\n/**\n * Hand the system call's audio session to WebRTC: CallKit activates it (so\n * audio keeps running with the phone locked and follows AirPods or CarPlay),\n * and WebRTC starts and stops its voice-processing audio on those signals.\n */\nexport function connectCallAudioToWebRtc(call: NativeCallAdapter): () => void {\n return call.subscribe((event) => {\n if (event.type === \"audioSessionActivated\") RTCAudioSession.audioSessionDidActivate();\n else if (event.type === \"audioSessionDeactivated\") RTCAudioSession.audioSessionDidDeactivate();\n });\n}\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'OpenGeniCall'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = 'System voice calls (CallKit) for OpenGeni agents'
|
|
9
|
+
s.description = 'Reports OpenGeni realtime voice sessions as CallKit calls and forwards call, audio-session and launch events to JavaScript.'
|
|
10
|
+
s.license = 'Apache-2.0'
|
|
11
|
+
s.author = 'OpenGeni'
|
|
12
|
+
s.homepage = 'https://github.com/Cloudgeni-ai/opengeni'
|
|
13
|
+
s.platforms = { :ios => '15.1' }
|
|
14
|
+
s.swift_version = '5.9'
|
|
15
|
+
s.source = { git: 'https://github.com/Cloudgeni-ai/opengeni.git' }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
s.frameworks = 'AVFoundation', 'CallKit', 'Intents'
|
|
20
|
+
|
|
21
|
+
s.source_files = '**/*.swift'
|
|
22
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Intents
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/// Turns system call launches into start requests: a call from the Phone
|
|
6
|
+
/// app's recents or Siri (INStartCallIntent), and the home-screen quick action
|
|
7
|
+
/// whose type ends in ".call".
|
|
8
|
+
public class OpenGeniCallAppDelegateSubscriber: ExpoAppDelegateSubscriber {
|
|
9
|
+
public func application(
|
|
10
|
+
_ application: UIApplication,
|
|
11
|
+
continue userActivity: NSUserActivity,
|
|
12
|
+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
|
13
|
+
) -> Bool {
|
|
14
|
+
guard let intent = userActivity.interaction?.intent as? INStartCallIntent else { return false }
|
|
15
|
+
let target = intent.contacts?.first?.personHandle?.value
|
|
16
|
+
OpenGeniCallCenter.shared.requestStart(target: target == "opengeni" ? nil : target)
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public func application(
|
|
21
|
+
_ application: UIApplication,
|
|
22
|
+
performActionFor shortcutItem: UIApplicationShortcutItem,
|
|
23
|
+
completionHandler: @escaping (Bool) -> Void
|
|
24
|
+
) {
|
|
25
|
+
guard shortcutItem.type.hasSuffix(".call") else { return completionHandler(false) }
|
|
26
|
+
OpenGeniCallCenter.shared.requestStart(target: shortcutItem.userInfo?["target"] as? String)
|
|
27
|
+
completionHandler(true)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
import CallKit
|
|
3
|
+
import Foundation
|
|
4
|
+
|
|
5
|
+
/// One CallKit provider for the process. The app delegate subscriber can receive
|
|
6
|
+
/// a start request (Siri, Phone recents, a home-screen action) before the module
|
|
7
|
+
/// or JavaScript exists, so requests wait here until JavaScript observes them.
|
|
8
|
+
final class OpenGeniCallCenter: NSObject, CXProviderDelegate {
|
|
9
|
+
static let shared = OpenGeniCallCenter()
|
|
10
|
+
|
|
11
|
+
private let controller = CXCallController()
|
|
12
|
+
private var provider: CXProvider?
|
|
13
|
+
private var activeCalls = Set<UUID>()
|
|
14
|
+
private var pendingStart: [String: Any]?
|
|
15
|
+
private var routeObserver: NSObjectProtocol?
|
|
16
|
+
private var speaker = true
|
|
17
|
+
|
|
18
|
+
/// Set while JavaScript observes events; nil otherwise.
|
|
19
|
+
var emit: (([String: Any]) -> Void)? {
|
|
20
|
+
didSet {
|
|
21
|
+
if emit != nil, let pending = pendingStart {
|
|
22
|
+
pendingStart = nil
|
|
23
|
+
emit?(pending)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func configure(includeInRecents: Bool) {
|
|
29
|
+
let configuration = CXProviderConfiguration()
|
|
30
|
+
configuration.supportsVideo = false
|
|
31
|
+
configuration.maximumCallGroups = 1
|
|
32
|
+
configuration.maximumCallsPerCallGroup = 1
|
|
33
|
+
configuration.supportedHandleTypes = [.generic]
|
|
34
|
+
configuration.includesCallsInRecents = includeInRecents
|
|
35
|
+
if let provider {
|
|
36
|
+
provider.configuration = configuration
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
let created = CXProvider(configuration: configuration)
|
|
40
|
+
created.setDelegate(self, queue: nil)
|
|
41
|
+
provider = created
|
|
42
|
+
routeObserver = NotificationCenter.default.addObserver(
|
|
43
|
+
forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main
|
|
44
|
+
) { [weak self] _ in
|
|
45
|
+
self?.send(["type": "routeChanged", "route": Self.currentRoute()])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// MARK: Requests from JavaScript
|
|
50
|
+
|
|
51
|
+
func startCall(id: UUID, title: String, target: String?, completion: @escaping (Error?) -> Void) {
|
|
52
|
+
if provider == nil { configure(includeInRecents: true) }
|
|
53
|
+
let handle = CXHandle(type: .generic, value: target ?? "opengeni")
|
|
54
|
+
let action = CXStartCallAction(call: id, handle: handle)
|
|
55
|
+
action.contactIdentifier = title
|
|
56
|
+
controller.request(CXTransaction(action: action)) { [weak self] error in
|
|
57
|
+
DispatchQueue.main.async {
|
|
58
|
+
if error == nil {
|
|
59
|
+
self?.activeCalls.insert(id)
|
|
60
|
+
let update = CXCallUpdate()
|
|
61
|
+
update.remoteHandle = handle
|
|
62
|
+
update.localizedCallerName = title
|
|
63
|
+
update.hasVideo = false
|
|
64
|
+
update.supportsHolding = false
|
|
65
|
+
update.supportsGrouping = false
|
|
66
|
+
update.supportsUngrouping = false
|
|
67
|
+
update.supportsDTMF = false
|
|
68
|
+
self?.provider?.reportCall(with: id, updated: update)
|
|
69
|
+
}
|
|
70
|
+
completion(error)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func reportConnected(id: UUID) {
|
|
76
|
+
provider?.reportOutgoingCall(with: id, connectedAt: Date())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
func endCall(id: UUID, failed: Bool, completion: @escaping () -> Void) {
|
|
80
|
+
guard activeCalls.contains(id) else { return completion() }
|
|
81
|
+
if failed {
|
|
82
|
+
activeCalls.remove(id)
|
|
83
|
+
provider?.reportCall(with: id, endedAt: Date(), reason: .failed)
|
|
84
|
+
return completion()
|
|
85
|
+
}
|
|
86
|
+
controller.request(CXTransaction(action: CXEndCallAction(call: id))) { [weak self] error in
|
|
87
|
+
DispatchQueue.main.async {
|
|
88
|
+
if error != nil {
|
|
89
|
+
self?.activeCalls.remove(id)
|
|
90
|
+
self?.provider?.reportCall(with: id, endedAt: Date(), reason: .remoteEnded)
|
|
91
|
+
}
|
|
92
|
+
completion()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func setMuted(id: UUID, muted: Bool, completion: @escaping () -> Void) {
|
|
98
|
+
guard activeCalls.contains(id) else { return completion() }
|
|
99
|
+
controller.request(CXTransaction(action: CXSetMutedCallAction(call: id, muted: muted))) { _ in
|
|
100
|
+
DispatchQueue.main.async { completion() }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func setSpeaker(_ on: Bool) throws {
|
|
105
|
+
speaker = on
|
|
106
|
+
let session = AVAudioSession.sharedInstance()
|
|
107
|
+
try session.setCategory(.playAndRecord, mode: .voiceChat, options: Self.options(speaker: on))
|
|
108
|
+
try session.overrideOutputAudioPort(on ? .speaker : .none)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// MARK: Start requests from outside the app
|
|
112
|
+
|
|
113
|
+
func requestStart(target: String?) {
|
|
114
|
+
let event: [String: Any] = ["type": "startRequested", "target": target ?? NSNull()]
|
|
115
|
+
if let emit {
|
|
116
|
+
emit(event)
|
|
117
|
+
} else {
|
|
118
|
+
pendingStart = event
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
func takePendingStart() -> [String: Any]? {
|
|
123
|
+
defer { pendingStart = nil }
|
|
124
|
+
guard let pending = pendingStart else { return nil }
|
|
125
|
+
return ["target": pending["target"] ?? NSNull()]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// MARK: CXProviderDelegate
|
|
129
|
+
|
|
130
|
+
func providerDidReset(_ provider: CXProvider) {
|
|
131
|
+
for id in activeCalls { send(["type": "ended", "callId": id.uuidString.lowercased()]) }
|
|
132
|
+
activeCalls.removeAll()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
|
|
136
|
+
// CallKit activates this configuration; WebRTC's voice processing then
|
|
137
|
+
// provides echo cancellation for hands-free use.
|
|
138
|
+
let session = AVAudioSession.sharedInstance()
|
|
139
|
+
try? session.setCategory(.playAndRecord, mode: .voiceChat, options: Self.options(speaker: speaker))
|
|
140
|
+
provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: Date())
|
|
141
|
+
action.fulfill()
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
|
|
145
|
+
activeCalls.remove(action.callUUID)
|
|
146
|
+
send(["type": "ended", "callId": action.callUUID.uuidString.lowercased()])
|
|
147
|
+
action.fulfill()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) {
|
|
151
|
+
send([
|
|
152
|
+
"type": "muteChanged", "callId": action.callUUID.uuidString.lowercased(),
|
|
153
|
+
"muted": action.isMuted,
|
|
154
|
+
])
|
|
155
|
+
action.fulfill()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
|
|
159
|
+
send(["type": "audioSessionActivated"])
|
|
160
|
+
send(["type": "routeChanged", "route": Self.currentRoute()])
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) {
|
|
164
|
+
send(["type": "audioSessionDeactivated"])
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// MARK: Helpers
|
|
168
|
+
|
|
169
|
+
private func send(_ event: [String: Any]) {
|
|
170
|
+
DispatchQueue.main.async { [weak self] in self?.emit?(event) }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private static func options(speaker: Bool) -> AVAudioSession.CategoryOptions {
|
|
174
|
+
speaker ? [.allowBluetooth, .defaultToSpeaker] : [.allowBluetooth]
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static func currentRoute() -> String {
|
|
178
|
+
guard let output = AVAudioSession.sharedInstance().currentRoute.outputs.first else {
|
|
179
|
+
return "other"
|
|
180
|
+
}
|
|
181
|
+
switch output.portType {
|
|
182
|
+
case .builtInSpeaker: return "speaker"
|
|
183
|
+
case .builtInReceiver: return "receiver"
|
|
184
|
+
case .bluetoothHFP, .bluetoothA2DP, .bluetoothLE, .carAudio: return "bluetooth"
|
|
185
|
+
case .headphones, .usbAudio: return "headphones"
|
|
186
|
+
default: return "other"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class OpenGeniCallModule: Module {
|
|
4
|
+
private let center = OpenGeniCallCenter.shared
|
|
5
|
+
|
|
6
|
+
public func definition() -> ModuleDefinition {
|
|
7
|
+
Name("OpenGeniCall")
|
|
8
|
+
|
|
9
|
+
Events("onCallEvent")
|
|
10
|
+
|
|
11
|
+
OnStartObserving {
|
|
12
|
+
self.center.emit = { [weak self] event in self?.sendEvent("onCallEvent", event) }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
OnStopObserving {
|
|
16
|
+
self.center.emit = nil
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
Function("configure") { (includeInRecents: Bool) in
|
|
20
|
+
DispatchQueue.main.async { self.center.configure(includeInRecents: includeInRecents) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
AsyncFunction("startCall") { (callId: String, title: String, target: String?, promise: Promise) in
|
|
24
|
+
guard let id = UUID(uuidString: callId) else {
|
|
25
|
+
return promise.reject("ERR_CALL_ID", "Call id must be a UUID")
|
|
26
|
+
}
|
|
27
|
+
self.center.startCall(id: id, title: title, target: target) { error in
|
|
28
|
+
if let error {
|
|
29
|
+
promise.reject("ERR_CALL_START", error.localizedDescription)
|
|
30
|
+
} else {
|
|
31
|
+
promise.resolve(nil)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
.runOnQueue(.main)
|
|
36
|
+
|
|
37
|
+
Function("reportConnected") { (callId: String) in
|
|
38
|
+
guard let id = UUID(uuidString: callId) else { return }
|
|
39
|
+
DispatchQueue.main.async { self.center.reportConnected(id: id) }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
AsyncFunction("endCall") { (callId: String, failed: Bool, promise: Promise) in
|
|
43
|
+
guard let id = UUID(uuidString: callId) else { return promise.resolve(nil) }
|
|
44
|
+
self.center.endCall(id: id, failed: failed) { promise.resolve(nil) }
|
|
45
|
+
}
|
|
46
|
+
.runOnQueue(.main)
|
|
47
|
+
|
|
48
|
+
AsyncFunction("setMuted") { (callId: String, muted: Bool, promise: Promise) in
|
|
49
|
+
guard let id = UUID(uuidString: callId) else { return promise.resolve(nil) }
|
|
50
|
+
self.center.setMuted(id: id, muted: muted) { promise.resolve(nil) }
|
|
51
|
+
}
|
|
52
|
+
.runOnQueue(.main)
|
|
53
|
+
|
|
54
|
+
AsyncFunction("setSpeaker") { (on: Bool) in
|
|
55
|
+
try self.center.setSpeaker(on)
|
|
56
|
+
}
|
|
57
|
+
.runOnQueue(.main)
|
|
58
|
+
|
|
59
|
+
AsyncFunction("takePendingStartRequest") { () -> [String: Any]? in
|
|
60
|
+
self.center.takePendingStart()
|
|
61
|
+
}
|
|
62
|
+
.runOnQueue(.main)
|
|
63
|
+
}
|
|
64
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opengeni/react-native",
|
|
3
|
+
"version": "1.4.5-canary.37615713321001",
|
|
4
|
+
"description": "React Native and Expo kit for OpenGeni agents: the native session timeline, composer adapters, voice dictation, and realtime voice calls (CallKit) on the same SDK controllers as the web.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/Cloudgeni-ai/opengeni.git",
|
|
9
|
+
"directory": "packages/react-native"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src",
|
|
14
|
+
"ios",
|
|
15
|
+
"expo-module.config.json",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./adapters": {
|
|
28
|
+
"types": "./dist/adapters.d.ts",
|
|
29
|
+
"import": "./dist/adapters.js"
|
|
30
|
+
},
|
|
31
|
+
"./expo": {
|
|
32
|
+
"types": "./dist/expo.d.ts",
|
|
33
|
+
"import": "./dist/expo.js"
|
|
34
|
+
},
|
|
35
|
+
"./ui": {
|
|
36
|
+
"types": "./dist/ui/index.d.ts",
|
|
37
|
+
"import": "./dist/ui/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./timeline": {
|
|
40
|
+
"types": "./dist/timeline/index.d.ts",
|
|
41
|
+
"import": "./dist/timeline/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./timeline/markdown": {
|
|
44
|
+
"types": "./dist/timeline/markdown.d.ts",
|
|
45
|
+
"import": "./dist/timeline/markdown.js"
|
|
46
|
+
},
|
|
47
|
+
"./webrtc": {
|
|
48
|
+
"types": "./dist/webrtc.d.ts",
|
|
49
|
+
"import": "./dist/webrtc.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public",
|
|
54
|
+
"provenance": true
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"test": "bun test",
|
|
58
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
59
|
+
"build": "bun ../../scripts/build-typescript-package.ts",
|
|
60
|
+
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@opengeni/contracts": "1.4.5-canary.37615713321001",
|
|
64
|
+
"@opengeni/react": "1.4.5-canary.37615713321001",
|
|
65
|
+
"@opengeni/sdk": "1.4.5-canary.37615713321001",
|
|
66
|
+
"thinking-orbs": "^0.3.1"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"expo": ">=57",
|
|
70
|
+
"expo-audio": "*",
|
|
71
|
+
"expo-crypto": "*",
|
|
72
|
+
"expo-document-picker": "*",
|
|
73
|
+
"expo-file-system": "*",
|
|
74
|
+
"expo-glass-effect": "*",
|
|
75
|
+
"expo-image-picker": "*",
|
|
76
|
+
"expo-modules-core": "*",
|
|
77
|
+
"react": ">=19",
|
|
78
|
+
"react-native": ">=0.80",
|
|
79
|
+
"react-native-markdown-display": ">=7",
|
|
80
|
+
"react-native-reanimated": ">=4",
|
|
81
|
+
"react-native-safe-area-context": ">=5",
|
|
82
|
+
"react-native-svg": ">=15",
|
|
83
|
+
"react-native-webrtc": ">=124"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"react-native-markdown-display": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"react-native-webrtc": {
|
|
90
|
+
"optional": true
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|