@marktiderman/genesis-chat 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -13
- package/dist/{chunk-SAY24E6V.js → chunk-5D5A7I4Q.js} +166 -45
- package/dist/chunk-5D5A7I4Q.js.map +1 -0
- package/dist/{chunk-JQXZUEEL.js → chunk-7S6HYNWW.js} +382 -8
- package/dist/chunk-7S6HYNWW.js.map +1 -0
- package/dist/chunk-BJCE5O7V.js +70 -0
- package/dist/chunk-BJCE5O7V.js.map +1 -0
- package/dist/chunk-PZTOY2LY.js +576 -0
- package/dist/chunk-PZTOY2LY.js.map +1 -0
- package/dist/chunk-VEPZROQB.js +187 -0
- package/dist/chunk-VEPZROQB.js.map +1 -0
- package/dist/{client-BtdALFCx.d.ts → client-DMa2j7kz.d.ts} +152 -11
- package/dist/index.d.ts +78 -163
- package/dist/index.js +3 -2
- package/dist/native.d.ts +1910 -1
- package/dist/native.js +4900 -0
- package/dist/native.js.map +1 -1
- package/dist/outbox-B0y3phE0.d.ts +151 -0
- package/dist/push.d.ts +206 -0
- package/dist/push.js +3 -0
- package/dist/push.js.map +1 -0
- package/dist/react.d.ts +149 -7
- package/dist/react.js +3 -3
- package/dist/server.d.ts +510 -21
- package/dist/server.js +863 -21
- package/dist/server.js.map +1 -1
- package/dist/thread-DWXHKPdw.d.ts +57 -0
- package/dist/web.d.ts +313 -10
- package/dist/web.js +1731 -60
- package/dist/web.js.map +1 -1
- package/migrations/0002_genesis_chat_coaching.sql +934 -0
- package/migrations/tests/0002_genesis_chat_coaching.test.sql +265 -0
- package/package.json +74 -6
- package/src/__tests__/coaching-core.test.ts +332 -0
- package/src/__tests__/coaching-react.test.tsx +248 -0
- package/src/__tests__/coaching-web.test.tsx +449 -0
- package/src/__tests__/core.test.ts +91 -0
- package/src/__tests__/outbox.test.ts +122 -0
- package/src/__tests__/realtime.test.ts +70 -0
- package/src/__tests__/review-fixes-core.test.ts +193 -0
- package/src/__tests__/review-fixes-web.test.tsx +402 -0
- package/src/__tests__/server-effects.test.ts +158 -0
- package/src/__tests__/server-fakes.ts +90 -0
- package/src/__tests__/server-integrations.test.ts +247 -0
- package/src/__tests__/server-providers.test.ts +246 -0
- package/src/__tests__/server-runner.test.ts +277 -0
- package/src/__tests__/server-safety.test.ts +290 -0
- package/src/__tests__/web-markdown.test.tsx +88 -0
- package/src/__tests__/web.test.tsx +333 -0
- package/src/core/availability.ts +223 -0
- package/src/core/client.ts +757 -0
- package/src/core/extras.ts +106 -0
- package/src/core/media.ts +144 -0
- package/src/core/outbox.ts +429 -0
- package/src/core/thread.ts +171 -0
- package/src/core/types.ts +311 -0
- package/src/index.ts +13 -0
- package/src/native/__tests__/auto-retry.test.tsx +128 -0
- package/src/native/__tests__/composer-mic-lock.test.tsx +115 -0
- package/src/native/__tests__/composer-state.test.ts +188 -0
- package/src/native/__tests__/composer.test.tsx +133 -0
- package/src/native/__tests__/emoji.test.tsx +238 -0
- package/src/native/__tests__/inbox-screen.test.tsx +48 -0
- package/src/native/__tests__/message-bubble.test.tsx +127 -0
- package/src/native/__tests__/organise-logic.test.ts +164 -0
- package/src/native/__tests__/organise-screens.test.tsx +344 -0
- package/src/native/__tests__/photo-viewer.test.tsx +226 -0
- package/src/native/__tests__/push-payload.test.ts +66 -0
- package/src/native/__tests__/push.test.ts +209 -0
- package/src/native/__tests__/recorder-machine.test.ts +96 -0
- package/src/native/__tests__/rows-actions.test.ts +76 -0
- package/src/native/__tests__/thread-layout.test.ts +41 -0
- package/src/native/__tests__/thread-screen.test.tsx +116 -0
- package/src/native/__tests__/tus.test.ts +154 -0
- package/src/native/__tests__/use-voice-recorder.test.tsx +316 -0
- package/src/native/__tests__/video-media.test.tsx +421 -0
- package/src/native/__tests__/video-recorder-state.test.ts +87 -0
- package/src/native/__tests__/video-upload.test.ts +227 -0
- package/src/native/access-error.tsx +31 -0
- package/src/native/actions.ts +42 -0
- package/src/native/attachment-cards.tsx +116 -0
- package/src/native/auto-retry.ts +69 -0
- package/src/native/availability.tsx +376 -0
- package/src/native/broadcast-sheet.tsx +193 -0
- package/src/native/composer-state.ts +302 -0
- package/src/native/composer.tsx +453 -0
- package/src/native/context.tsx +70 -0
- package/src/native/copy.ts +296 -0
- package/src/native/emoji-data.ts +21 -0
- package/src/native/emoji-picker.tsx +229 -0
- package/src/native/emoji.ts +118 -0
- package/src/native/icon-button.tsx +40 -0
- package/src/native/inbox-screen.tsx +116 -0
- package/src/native/index.ts +184 -0
- package/src/native/link-preview.tsx +104 -0
- package/src/native/media-colors.ts +15 -0
- package/src/native/media.tsx +187 -0
- package/src/native/message-actions-sheet.tsx +121 -0
- package/src/native/message-bubble.tsx +374 -0
- package/src/native/moderation.tsx +42 -0
- package/src/native/modules.ts +275 -0
- package/src/native/organise-copy.ts +195 -0
- package/src/native/photo-viewer.tsx +531 -0
- package/src/native/push.ts +375 -0
- package/src/native/recorder-machine.ts +176 -0
- package/src/native/rows.ts +81 -0
- package/src/native/schedule-sheet.tsx +314 -0
- package/src/native/search-screen.tsx +168 -0
- package/src/native/stack-options.ts +36 -0
- package/src/native/start-sheet.tsx +87 -0
- package/src/native/thread-screen.tsx +620 -0
- package/src/native/topics.tsx +179 -0
- package/src/native/tus.ts +282 -0
- package/src/native/use-composer.ts +71 -0
- package/src/native/use-jump.ts +91 -0
- package/src/native/use-voice-recorder.ts +234 -0
- package/src/native/video-player.tsx +370 -0
- package/src/native/video-recorder-state.ts +140 -0
- package/src/native/video-recorder.tsx +271 -0
- package/src/native/video-upload-tray.tsx +77 -0
- package/src/native/video-upload.ts +432 -0
- package/src/native/voice-player.tsx +283 -0
- package/src/react/index.tsx +810 -0
- package/src/server/assistant.ts +235 -0
- package/src/server/crypto.ts +86 -0
- package/src/server/expo.ts +119 -0
- package/src/server/handler.ts +32 -0
- package/src/server/index.ts +57 -0
- package/src/server/moderation.ts +155 -0
- package/src/server/mux.ts +290 -0
- package/src/server/notifications.ts +130 -0
- package/src/server/runner.ts +468 -0
- package/src/server/transcribers.ts +111 -0
- package/src/server/types.ts +171 -0
- package/src/server/unfurl.ts +480 -0
- package/src/server/upload.ts +158 -0
- package/src/web/attachments.tsx +379 -0
- package/src/web/availability.tsx +299 -0
- package/src/web/broadcast.tsx +166 -0
- package/src/web/bubble.tsx +563 -0
- package/src/web/cards.tsx +136 -0
- package/src/web/composer.tsx +571 -0
- package/src/web/emoji-picker.tsx +84 -0
- package/src/web/error-boundary.tsx +33 -0
- package/src/web/format.ts +55 -0
- package/src/web/inbox.tsx +195 -0
- package/src/web/index.ts +75 -0
- package/src/web/markdown.tsx +245 -0
- package/src/web/memory-client.ts +706 -0
- package/src/web/panel.tsx +135 -0
- package/src/web/schedule.tsx +192 -0
- package/src/web/search.tsx +129 -0
- package/src/web/thread-context.ts +64 -0
- package/src/web/thread.tsx +802 -0
- package/src/web/topics.tsx +160 -0
- package/src/web/use-voice-recorder.ts +264 -0
- package/src/web/video.tsx +522 -0
- package/src/web/voice-note.tsx +197 -0
- package/dist/chunk-27UTR5FW.js +0 -322
- package/dist/chunk-27UTR5FW.js.map +0 -1
- package/dist/chunk-JQXZUEEL.js.map +0 -1
- package/dist/chunk-SAY24E6V.js.map +0 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @feat:chat
|
|
3
|
+
* The native modules chat can use, as the shapes it needs. The app passes the
|
|
4
|
+
* real ones (each an optional peer); chat never imports them, so it builds and
|
|
5
|
+
* runs without any of them, and the tests pass fakes:
|
|
6
|
+
*
|
|
7
|
+
* import * as Audio from "expo-audio"; // voice notes
|
|
8
|
+
* import * as ImagePicker from "expo-image-picker"; // photos and videos
|
|
9
|
+
* import * as Haptics from "expo-haptics"; // send and record
|
|
10
|
+
* import * as Clipboard from "expo-clipboard"; // copy, paste a picture
|
|
11
|
+
* import { KeyboardAvoidingView } from "react-native-keyboard-controller";
|
|
12
|
+
* import AsyncStorage from "@react-native-async-storage/async-storage"; // drafts
|
|
13
|
+
* import * as Sharing from "expo-sharing"; // share a photo
|
|
14
|
+
* import * as MediaLibrary from "expo-media-library"; // save a photo
|
|
15
|
+
* import NetInfo from "@react-native-community/netinfo"; // retry when back online
|
|
16
|
+
* import * as Camera from "expo-camera"; // video messages
|
|
17
|
+
* import * as Video from "expo-video"; // video playback
|
|
18
|
+
* import * as WebBrowser from "expo-web-browser"; // link previews
|
|
19
|
+
*
|
|
20
|
+
* FlashList and expo-image come from GenesisNativeApp's `modules` (genesis-ui-native's
|
|
21
|
+
* GenesisModulesProvider), the same objects every Genesis list and image uses.
|
|
22
|
+
* The shapes follow the SDK 58 packages' own type declarations.
|
|
23
|
+
*/
|
|
24
|
+
import type { ComponentType, ReactNode } from "react";
|
|
25
|
+
|
|
26
|
+
/** `RecorderState` from expo-audio (`recorder.getStatus()`). */
|
|
27
|
+
export interface RecorderStateLike {
|
|
28
|
+
isRecording: boolean;
|
|
29
|
+
durationMillis: number;
|
|
30
|
+
/** dB, about -160…0, when `isMeteringEnabled`. */
|
|
31
|
+
metering?: number;
|
|
32
|
+
url: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** `AudioRecorder` from expo-audio. */
|
|
36
|
+
export interface AudioRecorderLike {
|
|
37
|
+
prepareToRecordAsync(options?: Record<string, unknown>): Promise<void>;
|
|
38
|
+
record(): void;
|
|
39
|
+
stop(): Promise<void>;
|
|
40
|
+
getStatus(): RecorderStateLike;
|
|
41
|
+
uri: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `AudioStatus` from expo-audio (`useAudioPlayerStatus`). Seconds. */
|
|
45
|
+
export interface AudioStatusLike {
|
|
46
|
+
currentTime: number;
|
|
47
|
+
duration: number;
|
|
48
|
+
playing: boolean;
|
|
49
|
+
didJustFinish: boolean;
|
|
50
|
+
isLoaded: boolean;
|
|
51
|
+
playbackRate: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `AudioPlayer` from expo-audio. */
|
|
55
|
+
export interface AudioPlayerLike {
|
|
56
|
+
play(): void;
|
|
57
|
+
pause(): void;
|
|
58
|
+
seekTo(seconds: number): Promise<void>;
|
|
59
|
+
setPlaybackRate(rate: number): void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `import * as Audio from "expo-audio"` (SDK 58). */
|
|
63
|
+
export interface ExpoAudioLike {
|
|
64
|
+
useAudioRecorder(options: Record<string, unknown>): AudioRecorderLike;
|
|
65
|
+
useAudioPlayer(source: { uri: string } | null, options?: { updateInterval?: number }): AudioPlayerLike;
|
|
66
|
+
useAudioPlayerStatus(player: AudioPlayerLike): AudioStatusLike;
|
|
67
|
+
RecordingPresets: { HIGH_QUALITY: Record<string, unknown> };
|
|
68
|
+
requestRecordingPermissionsAsync(): Promise<{ granted: boolean; canAskAgain?: boolean }>;
|
|
69
|
+
setAudioModeAsync(mode: { allowsRecording?: boolean; playsInSilentMode?: boolean }): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** One picked file (`ImagePickerAsset`). */
|
|
73
|
+
export interface PickedAssetLike {
|
|
74
|
+
uri: string;
|
|
75
|
+
width?: number;
|
|
76
|
+
height?: number;
|
|
77
|
+
fileName?: string | null;
|
|
78
|
+
fileSize?: number;
|
|
79
|
+
mimeType?: string;
|
|
80
|
+
/** Milliseconds, for a video. */
|
|
81
|
+
duration?: number | null;
|
|
82
|
+
type?: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PickerResultLike {
|
|
86
|
+
canceled: boolean;
|
|
87
|
+
assets: PickedAssetLike[] | null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** `import * as ImagePicker from "expo-image-picker"`. */
|
|
91
|
+
export interface ImagePickerLike {
|
|
92
|
+
launchImageLibraryAsync(options?: Record<string, unknown>): Promise<PickerResultLike>;
|
|
93
|
+
launchCameraAsync?(options?: Record<string, unknown>): Promise<PickerResultLike>;
|
|
94
|
+
requestCameraPermissionsAsync?(): Promise<{ granted: boolean }>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** `import * as Haptics from "expo-haptics"`. */
|
|
98
|
+
export interface HapticsLike {
|
|
99
|
+
impactAsync(style?: never): Promise<void>;
|
|
100
|
+
selectionAsync(): Promise<void>;
|
|
101
|
+
notificationAsync(type?: never): Promise<void>;
|
|
102
|
+
ImpactFeedbackStyle: { Light: unknown; Medium: unknown };
|
|
103
|
+
NotificationFeedbackType: { Success: unknown; Warning: unknown; Error: unknown };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `import * as Clipboard from "expo-clipboard"`. */
|
|
107
|
+
export interface ClipboardLike {
|
|
108
|
+
setStringAsync(text: string): Promise<boolean>;
|
|
109
|
+
hasImageAsync?(): Promise<boolean>;
|
|
110
|
+
/** `data` is a data URI (`data:image/png;base64,…`). */
|
|
111
|
+
getImageAsync?(options: { format: "png" | "jpeg" }): Promise<{ data: string; size: { width: number; height: number } } | null>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** react-native-keyboard-controller's `KeyboardAvoidingView` (needs its `KeyboardProvider`, which GenesisNativeApp mounts). */
|
|
115
|
+
export type KeyboardAvoidingLike = ComponentType<{
|
|
116
|
+
behavior?: "height" | "padding" | "position" | "translate-with-padding";
|
|
117
|
+
keyboardVerticalOffset?: number;
|
|
118
|
+
style?: unknown;
|
|
119
|
+
children?: ReactNode;
|
|
120
|
+
testID?: string;
|
|
121
|
+
}>;
|
|
122
|
+
|
|
123
|
+
/** Where drafts wait: AsyncStorage, or MMKV's async adapter. */
|
|
124
|
+
export interface DraftStorageLike {
|
|
125
|
+
getItem(key: string): Promise<string | null>;
|
|
126
|
+
setItem(key: string, value: string): Promise<void>;
|
|
127
|
+
removeItem(key: string): Promise<void>;
|
|
128
|
+
/** For `clearDrafts` on sign-out. */
|
|
129
|
+
getAllKeys?(): Promise<readonly string[]>;
|
|
130
|
+
multiRemove?(keys: readonly string[]): Promise<void>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** `import * as Sharing from "expo-sharing"`: the system share sheet for a file on the phone. */
|
|
134
|
+
export interface SharingLike {
|
|
135
|
+
isAvailableAsync(): Promise<boolean>;
|
|
136
|
+
shareAsync(url: string, options?: { mimeType?: string; UTI?: string; dialogTitle?: string }): Promise<void>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** `import * as MediaLibrary from "expo-media-library"`: save to Photos (asks for add-only access). */
|
|
140
|
+
export interface MediaLibraryLike {
|
|
141
|
+
requestPermissionsAsync(writeOnly?: boolean): Promise<{ granted: boolean }>;
|
|
142
|
+
saveToLibraryAsync(localUri: string): Promise<void>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** `NetInfo` from @react-native-community/netinfo: tells chat when the connection comes back. */
|
|
146
|
+
export interface NetInfoLike {
|
|
147
|
+
addEventListener(listener: (state: { isConnected: boolean | null; isInternetReachable?: boolean | null }) => void): () => void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A permission answer from expo-camera (`PermissionResponse`). */
|
|
151
|
+
export interface PermissionLike {
|
|
152
|
+
granted: boolean;
|
|
153
|
+
canAskAgain?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** The `ref` of expo-camera's `CameraView`. */
|
|
157
|
+
export interface CameraViewHandleLike {
|
|
158
|
+
/** Resolves with the file when `stopRecording` is called, the limit is reached or the preview stops. */
|
|
159
|
+
recordAsync(options?: { maxDuration?: number; maxFileSize?: number; codec?: string }): Promise<{ uri: string } | undefined>;
|
|
160
|
+
stopRecording(): void;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** `import * as Camera from "expo-camera"` (SDK 58): `CameraView` and the permission requests on `Camera`. */
|
|
164
|
+
export interface ExpoCameraLike {
|
|
165
|
+
CameraView: ComponentType<{
|
|
166
|
+
ref?: unknown;
|
|
167
|
+
style?: unknown;
|
|
168
|
+
facing?: "front" | "back";
|
|
169
|
+
mode?: "picture" | "video";
|
|
170
|
+
mirror?: boolean;
|
|
171
|
+
mute?: boolean;
|
|
172
|
+
videoQuality?: "2160p" | "1080p" | "720p" | "480p" | "4:3";
|
|
173
|
+
onCameraReady?: () => void;
|
|
174
|
+
onMountError?: (event: { message: string }) => void;
|
|
175
|
+
testID?: string;
|
|
176
|
+
}>;
|
|
177
|
+
Camera: {
|
|
178
|
+
requestCameraPermissionsAsync(): Promise<PermissionLike>;
|
|
179
|
+
requestMicrophonePermissionsAsync(): Promise<PermissionLike>;
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** `VideoPlayer` from expo-video. */
|
|
184
|
+
export interface VideoPlayerLike {
|
|
185
|
+
play(): void;
|
|
186
|
+
pause(): void;
|
|
187
|
+
muted: boolean;
|
|
188
|
+
playing: boolean;
|
|
189
|
+
playbackRate: number;
|
|
190
|
+
currentTime: number;
|
|
191
|
+
addListener(event: "playingChange" | "statusChange" | "playToEnd", listener: (payload: never) => void): { remove(): void };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** `import * as Video from "expo-video"` (SDK 58). */
|
|
195
|
+
export interface ExpoVideoLike {
|
|
196
|
+
useVideoPlayer(source: { uri: string; contentType?: "auto" | "progressive" | "hls" } | null, setup?: (player: VideoPlayerLike) => void): VideoPlayerLike;
|
|
197
|
+
VideoView: ComponentType<{
|
|
198
|
+
ref?: unknown;
|
|
199
|
+
player: VideoPlayerLike;
|
|
200
|
+
style?: unknown;
|
|
201
|
+
contentFit?: "contain" | "cover" | "fill";
|
|
202
|
+
nativeControls?: boolean;
|
|
203
|
+
allowsPictureInPicture?: boolean;
|
|
204
|
+
testID?: string;
|
|
205
|
+
}>;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** The `ref` of expo-video's `VideoView`. */
|
|
209
|
+
export interface VideoViewHandleLike {
|
|
210
|
+
enterFullscreen(): Promise<void>;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** `import * as WebBrowser from "expo-web-browser"`: links open in an in-app browser. */
|
|
214
|
+
export interface WebBrowserLike {
|
|
215
|
+
openBrowserAsync(url: string, options?: Record<string, unknown>): Promise<unknown>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** An open file read a chunk at a time (expo-file-system's `new File(uri).open()`), as `createTusUpload` takes. */
|
|
219
|
+
export interface ChunkFileLike {
|
|
220
|
+
size: number;
|
|
221
|
+
read(offset: number, length: number): Promise<ArrayBuffer | Uint8Array>;
|
|
222
|
+
close?(): void;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Each optional: without one its feature is hidden (no mic without audio, no attach without a picker). */
|
|
226
|
+
export interface ChatNativeModules {
|
|
227
|
+
audio?: ExpoAudioLike;
|
|
228
|
+
imagePicker?: ImagePickerLike;
|
|
229
|
+
haptics?: HapticsLike;
|
|
230
|
+
clipboard?: ClipboardLike;
|
|
231
|
+
keyboardAvoidingView?: KeyboardAvoidingLike;
|
|
232
|
+
drafts?: DraftStorageLike;
|
|
233
|
+
/** Share in the photo viewer (with `downloadFile` for photos not on the phone). */
|
|
234
|
+
sharing?: SharingLike;
|
|
235
|
+
/** Save in the photo viewer (with `downloadFile`). */
|
|
236
|
+
mediaLibrary?: MediaLibraryLike;
|
|
237
|
+
/**
|
|
238
|
+
* Copies a photo from its signed link to the phone and returns the file:// URI, for sharing
|
|
239
|
+
* and saving (expo-file-system: `(await File.downloadFileAsync(url, Paths.cache, { idempotent: true })).uri`).
|
|
240
|
+
*/
|
|
241
|
+
downloadFile?: (url: string) => Promise<string>;
|
|
242
|
+
/** Unsent messages are retried when the connection comes back (`ChatAutoRetry`). */
|
|
243
|
+
netInfo?: NetInfoLike;
|
|
244
|
+
/** Video messages recorded in the thread. Without it there is no video button. */
|
|
245
|
+
camera?: ExpoCameraLike;
|
|
246
|
+
/** Video playback (HLS and files) in the bubble. Without it a video shows as a row with its name. */
|
|
247
|
+
video?: ExpoVideoLike;
|
|
248
|
+
/** Where links from previews open; without it, the system browser (`Linking`). */
|
|
249
|
+
webBrowser?: WebBrowserLike;
|
|
250
|
+
/** Reads a recording a chunk at a time for a hosted upload; without it the file is read whole. */
|
|
251
|
+
openFile?: (uri: string) => Promise<ChunkFileLike> | ChunkFileLike;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type HapticEvent = "send" | "record-start" | "record-lock" | "record-cancel" | "select" | "error";
|
|
255
|
+
|
|
256
|
+
/** Plays a haptic when the app gave expo-haptics; never throws. */
|
|
257
|
+
export function haptic(haptics: HapticsLike | undefined, event: HapticEvent): void {
|
|
258
|
+
if (!haptics) return;
|
|
259
|
+
try {
|
|
260
|
+
const style = (s: unknown) => s as never;
|
|
261
|
+
const p =
|
|
262
|
+
event === "send" || event === "record-lock"
|
|
263
|
+
? haptics.impactAsync(style(haptics.ImpactFeedbackStyle.Light))
|
|
264
|
+
: event === "record-start"
|
|
265
|
+
? haptics.impactAsync(style(haptics.ImpactFeedbackStyle.Medium))
|
|
266
|
+
: event === "select"
|
|
267
|
+
? haptics.selectionAsync()
|
|
268
|
+
: haptics.notificationAsync(
|
|
269
|
+
style(event === "error" ? haptics.NotificationFeedbackType.Error : haptics.NotificationFeedbackType.Warning),
|
|
270
|
+
);
|
|
271
|
+
void p?.catch?.(() => undefined);
|
|
272
|
+
} catch {
|
|
273
|
+
// A haptic is never worth an error.
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @feat:chat
|
|
3
|
+
* The words of chat's organising features on native (topics, scheduled send, availability,
|
|
4
|
+
* broadcast, search, moderation), part of `ChatNativeCopy`, so an app overrides them the same
|
|
5
|
+
* way (`ChatNativeProvider copy`). `{name}`, `{count}`, `{when}` and `{query}` are filled in.
|
|
6
|
+
*/
|
|
7
|
+
export interface ChatOrganiseCopy {
|
|
8
|
+
// Topics
|
|
9
|
+
topics: string;
|
|
10
|
+
topicAll: string;
|
|
11
|
+
topicClosed: string;
|
|
12
|
+
topicOptions: string;
|
|
13
|
+
topicNew: string;
|
|
14
|
+
topicRename: string;
|
|
15
|
+
topicClose: string;
|
|
16
|
+
topicReopen: string;
|
|
17
|
+
topicName: string;
|
|
18
|
+
topicNamePlaceholder: string;
|
|
19
|
+
topicAdd: string;
|
|
20
|
+
topicSave: string;
|
|
21
|
+
topicExists: string;
|
|
22
|
+
topicClosedNotice: string;
|
|
23
|
+
topicPlaceholder: string;
|
|
24
|
+
topicClosedDone: string;
|
|
25
|
+
topicReopenedDone: string;
|
|
26
|
+
// Scheduled send
|
|
27
|
+
schedule: string;
|
|
28
|
+
scheduleTitle: string;
|
|
29
|
+
scheduleHint: string;
|
|
30
|
+
schedulePick: string;
|
|
31
|
+
scheduleDate: string;
|
|
32
|
+
scheduleTime: string;
|
|
33
|
+
scheduleConfirm: string;
|
|
34
|
+
scheduleOutOfRange: string;
|
|
35
|
+
scheduledFor: string;
|
|
36
|
+
scheduleFailed: string;
|
|
37
|
+
scheduleNoFiles: string;
|
|
38
|
+
scheduledCount: string;
|
|
39
|
+
scheduledCountOne: string;
|
|
40
|
+
scheduledTitle: string;
|
|
41
|
+
scheduledEmpty: string;
|
|
42
|
+
scheduledEmptyBody: string;
|
|
43
|
+
scheduledCancel: string;
|
|
44
|
+
/** On a scheduled message that couldn't be sent. */
|
|
45
|
+
scheduledRemove: string;
|
|
46
|
+
scheduledCancelled: string;
|
|
47
|
+
scheduledCouldntSend: string;
|
|
48
|
+
// Availability
|
|
49
|
+
availabilityTitle: string;
|
|
50
|
+
awayMessage: string;
|
|
51
|
+
awayMessagePlaceholder: string;
|
|
52
|
+
awayUntil: string;
|
|
53
|
+
awayUntilOn: string;
|
|
54
|
+
imBack: string;
|
|
55
|
+
workingHours: string;
|
|
56
|
+
workingHoursOn: string;
|
|
57
|
+
workingHoursHint: string;
|
|
58
|
+
workingDay: string;
|
|
59
|
+
notWorking: string;
|
|
60
|
+
hoursFrom: string;
|
|
61
|
+
hoursTo: string;
|
|
62
|
+
hoursSameTime: string;
|
|
63
|
+
timeZone: string;
|
|
64
|
+
timeZoneSearch: string;
|
|
65
|
+
availabilitySave: string;
|
|
66
|
+
availabilitySaved: string;
|
|
67
|
+
// Broadcast
|
|
68
|
+
broadcastTitle: string;
|
|
69
|
+
broadcastExplainer: string;
|
|
70
|
+
broadcastMessage: string;
|
|
71
|
+
broadcastPlaceholder: string;
|
|
72
|
+
broadcastTo: string;
|
|
73
|
+
broadcastFilter: string;
|
|
74
|
+
broadcastSelectAll: string;
|
|
75
|
+
broadcastClear: string;
|
|
76
|
+
broadcastPickSome: string;
|
|
77
|
+
broadcastPicked: string;
|
|
78
|
+
broadcastPickedOne: string;
|
|
79
|
+
broadcastSend: string;
|
|
80
|
+
broadcastSendTo: string;
|
|
81
|
+
broadcastSent: string;
|
|
82
|
+
broadcastSentOne: string;
|
|
83
|
+
broadcastSentSome: string;
|
|
84
|
+
broadcastNoMatches: string;
|
|
85
|
+
broadcastNoConversations: string;
|
|
86
|
+
// Search
|
|
87
|
+
search: string;
|
|
88
|
+
searchPlaceholder: string;
|
|
89
|
+
searchThreadPlaceholder: string;
|
|
90
|
+
searchHint: string;
|
|
91
|
+
searchNoResults: string;
|
|
92
|
+
searchNoResultsBody: string;
|
|
93
|
+
searchResults: string;
|
|
94
|
+
searchResultsOne: string;
|
|
95
|
+
searchIn: string;
|
|
96
|
+
transcript: string;
|
|
97
|
+
messageGone: string;
|
|
98
|
+
/** Said by VoiceOver when a jump lands. */
|
|
99
|
+
jumpedTo: string;
|
|
100
|
+
// Moderation
|
|
101
|
+
moderationHidden: string;
|
|
102
|
+
moderationHiddenMine: string;
|
|
103
|
+
moderationFlagged: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const chatOrganiseCopy: ChatOrganiseCopy = {
|
|
107
|
+
topics: "Topics",
|
|
108
|
+
topicAll: "All",
|
|
109
|
+
topicClosed: "Closed",
|
|
110
|
+
topicOptions: "Topic options",
|
|
111
|
+
topicNew: "New topic",
|
|
112
|
+
topicRename: "Rename topic",
|
|
113
|
+
topicClose: "Close topic",
|
|
114
|
+
topicReopen: "Reopen topic",
|
|
115
|
+
topicName: "Name",
|
|
116
|
+
topicNamePlaceholder: "Week 1: Habits",
|
|
117
|
+
topicAdd: "Add topic",
|
|
118
|
+
topicSave: "Save",
|
|
119
|
+
topicExists: "There's already a topic with that name.",
|
|
120
|
+
topicClosedNotice: "This topic is closed.",
|
|
121
|
+
topicPlaceholder: "Message #{name}",
|
|
122
|
+
topicClosedDone: "Closed {name}",
|
|
123
|
+
topicReopenedDone: "Reopened {name}",
|
|
124
|
+
schedule: "Schedule message",
|
|
125
|
+
scheduleTitle: "Schedule message",
|
|
126
|
+
scheduleHint: "It sends at this time, in your time zone, as if you'd sent it then.",
|
|
127
|
+
schedulePick: "Pick a date and time",
|
|
128
|
+
scheduleDate: "Date",
|
|
129
|
+
scheduleTime: "Time",
|
|
130
|
+
scheduleConfirm: "Schedule",
|
|
131
|
+
scheduleOutOfRange: "Pick a time from a minute to a year from now.",
|
|
132
|
+
scheduledFor: "Scheduled for {when}",
|
|
133
|
+
scheduleFailed: "Couldn't schedule it",
|
|
134
|
+
scheduleNoFiles: "Photos and files can't be scheduled yet. Send them now, or remove them to schedule the text.",
|
|
135
|
+
scheduledCount: "{count} scheduled messages",
|
|
136
|
+
scheduledCountOne: "1 scheduled message",
|
|
137
|
+
scheduledTitle: "Scheduled",
|
|
138
|
+
scheduledEmpty: "Nothing scheduled",
|
|
139
|
+
scheduledEmptyBody: "Touch and hold Send to schedule a message.",
|
|
140
|
+
scheduledCancel: "Cancel",
|
|
141
|
+
scheduledRemove: "Remove",
|
|
142
|
+
scheduledCancelled: "Scheduled message cancelled",
|
|
143
|
+
scheduledCouldntSend: "Couldn't send",
|
|
144
|
+
availabilityTitle: "Availability",
|
|
145
|
+
awayMessage: "Away message",
|
|
146
|
+
awayMessagePlaceholder: "Back Monday. For anything urgent, call the office.",
|
|
147
|
+
awayUntil: "Away until",
|
|
148
|
+
awayUntilOn: "Set an end",
|
|
149
|
+
imBack: "I'm back",
|
|
150
|
+
workingHours: "Working hours",
|
|
151
|
+
workingHoursOn: "Tell people when you usually reply",
|
|
152
|
+
workingHoursHint: "An end before the start runs past midnight.",
|
|
153
|
+
workingDay: "Working on {name}",
|
|
154
|
+
notWorking: "Not working",
|
|
155
|
+
hoursFrom: "{name} from",
|
|
156
|
+
hoursTo: "{name} to",
|
|
157
|
+
hoursSameTime: "A day's start and end can't be the same time.",
|
|
158
|
+
timeZone: "Time zone",
|
|
159
|
+
timeZoneSearch: "Search time zones",
|
|
160
|
+
availabilitySave: "Save",
|
|
161
|
+
availabilitySaved: "Availability saved",
|
|
162
|
+
broadcastTitle: "Broadcast",
|
|
163
|
+
broadcastExplainer: "Each person gets this in their own conversation with you, so replies come back to you privately. No one sees who else got it.",
|
|
164
|
+
broadcastMessage: "Message",
|
|
165
|
+
broadcastPlaceholder: "Write once, send to everyone you pick",
|
|
166
|
+
broadcastTo: "Send to",
|
|
167
|
+
broadcastFilter: "Filter",
|
|
168
|
+
broadcastSelectAll: "Select all",
|
|
169
|
+
broadcastClear: "Clear",
|
|
170
|
+
broadcastPickSome: "Pick who gets it",
|
|
171
|
+
broadcastPicked: "{count} conversations",
|
|
172
|
+
broadcastPickedOne: "1 conversation",
|
|
173
|
+
broadcastSend: "Send",
|
|
174
|
+
broadcastSendTo: "Send to {count}",
|
|
175
|
+
broadcastSent: "Sent to {count} conversations",
|
|
176
|
+
broadcastSentOne: "Sent to 1 conversation",
|
|
177
|
+
broadcastSentSome: "Sent to {count} of {total}. You can't post in the others.",
|
|
178
|
+
broadcastNoMatches: "No matches",
|
|
179
|
+
broadcastNoConversations: "No conversations yet",
|
|
180
|
+
search: "Search",
|
|
181
|
+
searchPlaceholder: "Search messages",
|
|
182
|
+
searchThreadPlaceholder: "Search this conversation",
|
|
183
|
+
searchHint: "Type at least two letters.",
|
|
184
|
+
searchNoResults: "No results",
|
|
185
|
+
searchNoResultsBody: "Nothing matches “{query}”.",
|
|
186
|
+
searchResults: "{count} results",
|
|
187
|
+
searchResultsOne: "1 result",
|
|
188
|
+
searchIn: "in {name}",
|
|
189
|
+
transcript: "Transcript",
|
|
190
|
+
messageGone: "That message isn't here any more",
|
|
191
|
+
jumpedTo: "Message from {name}: {text}",
|
|
192
|
+
moderationHidden: "Hidden by moderation",
|
|
193
|
+
moderationHiddenMine: "Hidden by moderation. Only you can see it.",
|
|
194
|
+
moderationFlagged: "Flagged",
|
|
195
|
+
};
|