@repzo/widget-react-native 1.2.0 → 1.2.2
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/CHANGELOG.md +57 -0
- package/README.md +73 -1
- package/lib/commonjs/api/client.js +57 -2
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/audio/voice.js +119 -0
- package/lib/commonjs/audio/voice.js.map +1 -0
- package/lib/commonjs/i18n.js +6 -0
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/index.js +2 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/locales.js +4 -0
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/push/push-notifications.js +61 -6
- package/lib/commonjs/push/push-notifications.js.map +1 -1
- package/lib/commonjs/state/use-widget-state.js +31 -0
- package/lib/commonjs/state/use-widget-state.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +22 -1
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +6 -0
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/FileCard.js +134 -0
- package/lib/commonjs/ui/components/FileCard.js.map +1 -0
- package/lib/commonjs/ui/components/Launcher.js +15 -1
- package/lib/commonjs/ui/components/Launcher.js.map +1 -1
- package/lib/commonjs/ui/components/VoiceNote.js +354 -0
- package/lib/commonjs/ui/components/VoiceNote.js.map +1 -0
- package/lib/commonjs/ui/components/icons.js +57 -0
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js +33 -14
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +0 -9
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/api/client.js +58 -2
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/audio/voice.js +110 -0
- package/lib/module/audio/voice.js.map +1 -0
- package/lib/module/i18n.js +6 -0
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/locales.js +4 -0
- package/lib/module/locales.js.map +1 -1
- package/lib/module/push/push-notifications.js +61 -6
- package/lib/module/push/push-notifications.js.map +1 -1
- package/lib/module/state/use-widget-state.js +30 -1
- package/lib/module/state/use-widget-state.js.map +1 -1
- package/lib/module/state/widget-store.js +22 -1
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +6 -0
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/FileCard.js +130 -0
- package/lib/module/ui/components/FileCard.js.map +1 -0
- package/lib/module/ui/components/Launcher.js +16 -2
- package/lib/module/ui/components/Launcher.js.map +1 -1
- package/lib/module/ui/components/VoiceNote.js +350 -0
- package/lib/module/ui/components/VoiceNote.js.map +1 -0
- package/lib/module/ui/components/icons.js +55 -0
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.js +33 -14
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +0 -9
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/typescript/commonjs/api/client.d.ts +14 -0
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/audio/voice.d.ts +29 -0
- package/lib/typescript/commonjs/audio/voice.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n.d.ts +4 -0
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +13 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/push/push-notifications.d.ts +21 -1
- package/lib/typescript/commonjs/push/push-notifications.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/use-widget-state.d.ts +8 -0
- package/lib/typescript/commonjs/state/use-widget-state.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +2 -0
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/FileCard.d.ts +21 -0
- package/lib/typescript/commonjs/ui/components/FileCard.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/Launcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts +32 -0
- package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts +4 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +0 -9
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +4 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/api/client.d.ts +14 -0
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/audio/voice.d.ts +29 -0
- package/lib/typescript/module/audio/voice.d.ts.map +1 -0
- package/lib/typescript/module/i18n.d.ts +4 -0
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +13 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/push/push-notifications.d.ts +21 -1
- package/lib/typescript/module/push/push-notifications.d.ts.map +1 -1
- package/lib/typescript/module/state/use-widget-state.d.ts +8 -0
- package/lib/typescript/module/state/use-widget-state.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +2 -0
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FileCard.d.ts +21 -0
- package/lib/typescript/module/ui/components/FileCard.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/Launcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/VoiceNote.d.ts +32 -0
- package/lib/typescript/module/ui/components/VoiceNote.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/icons.d.ts +4 -0
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +0 -9
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +4 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/api/client.ts +61 -2
- package/src/audio/voice.ts +103 -0
- package/src/i18n.ts +6 -0
- package/src/index.tsx +13 -0
- package/src/locales.ts +4 -0
- package/src/push/push-notifications.ts +68 -5
- package/src/state/use-widget-state.ts +30 -1
- package/src/state/widget-store.ts +22 -1
- package/src/ui/WidgetProvider.tsx +6 -0
- package/src/ui/components/FileCard.tsx +111 -0
- package/src/ui/components/Launcher.tsx +7 -2
- package/src/ui/components/VoiceNote.tsx +363 -0
- package/src/ui/components/icons.tsx +33 -0
- package/src/ui/screens/chat/ChatView.styles.ts +0 -9
- package/src/ui/screens/chat/ChatView.tsx +39 -13
- package/src/ui/screens/chat/ChatView.types.ts +4 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A voice note (or any audio attachment) played inside the chat, laid out the
|
|
3
|
+
* way messaging apps do it: a play button beside a waveform the visitor can
|
|
4
|
+
* tap or drag to jump through the note, the time under it, and a speed chip
|
|
5
|
+
* once the note has been started. Only rendered when the host app has
|
|
6
|
+
* `expo-audio` (see `src/audio/voice.ts`); without it audio stays a file row.
|
|
7
|
+
*/
|
|
8
|
+
export interface VoiceNoteLabels {
|
|
9
|
+
play: string;
|
|
10
|
+
pause: string;
|
|
11
|
+
speed: string;
|
|
12
|
+
}
|
|
13
|
+
interface VoiceNoteProps {
|
|
14
|
+
url: string;
|
|
15
|
+
/** Keeps the made-up waveform the same for this note on every render. */
|
|
16
|
+
seed: string;
|
|
17
|
+
/** The workspace color. */
|
|
18
|
+
color: string;
|
|
19
|
+
/** In a visitor bubble, which is filled with `color`: the player turns white. */
|
|
20
|
+
onBrandBubble: boolean;
|
|
21
|
+
labels: VoiceNoteLabels;
|
|
22
|
+
/** The note can't be played (expired link, unsupported format). */
|
|
23
|
+
onError: () => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The player is only created once the visitor taps play. The whole thread
|
|
27
|
+
* renders at once, so a player per note would download every note in the
|
|
28
|
+
* conversation as soon as the chat opened.
|
|
29
|
+
*/
|
|
30
|
+
export declare function VoiceNote(props: VoiceNoteProps): import("react").JSX.Element;
|
|
31
|
+
export {};
|
|
32
|
+
//# sourceMappingURL=VoiceNote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"VoiceNote.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/VoiceNote.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAwBH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,aAAa,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,eAAe,CAAC;IACxB,mEAAmE;IACnE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAUD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,+BAa9C"}
|
|
@@ -18,6 +18,10 @@ type ChevronDirection = 'left' | 'right' | 'up' | 'down';
|
|
|
18
18
|
export declare function ChevronIcon({ size, color, direction, style, }: IconProps & {
|
|
19
19
|
direction?: ChevronDirection;
|
|
20
20
|
}): import("react").JSX.Element;
|
|
21
|
+
/** A right-pointing triangle, drawn with borders — a voice note's play button. */
|
|
22
|
+
export declare function PlayIcon({ size, color, style }: IconProps): import("react").JSX.Element;
|
|
23
|
+
/** Two upright bars — a playing voice note's pause button. */
|
|
24
|
+
export declare function PauseIcon({ size, color, style }: IconProps): import("react").JSX.Element;
|
|
21
25
|
/** Arrow with a shaft — used for the "start a conversation" affordance. */
|
|
22
26
|
export declare function ArrowRightIcon({ size, color, style }: IconProps): import("react").JSX.Element;
|
|
23
27
|
/** Right-pointing triangle for the send button. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/icons.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhE,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,wBAAwB;AACxB,wBAAgB,SAAS,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAcnF;AAED,KAAK,gBAAgB,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;AASzD,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,EAC1B,IAAS,EACT,KAAyB,EACzB,SAAkB,EAClB,KAAK,GACN,EAAE,SAAS,GAAG;IAAE,SAAS,CAAC,EAAE,gBAAgB,CAAA;CAAE,+BAgB9C;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA2BxF;AAED,mDAAmD;AACnD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA2BjF;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA4B9E;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,EAAE,IAAS,EAAE,KAA0B,EAAE,KAAK,EAAE,EAAE,SAAS,+BA6B1F;AAED,oCAAoC;AACpC,wBAAgB,QAAQ,CAAC,EAAE,IAAS,EAAE,KAA0B,EAAE,KAAK,EAAE,EAAE,SAAS,+BA6BnF;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA8BhF;AAED,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,EAAE,IAAS,EAAE,KAA2B,EAAE,KAAK,EAAE,EAAE,SAAS,+BA+BrF;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAgBvF;AASD,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAkCnF"}
|
|
1
|
+
{"version":3,"file":"icons.d.ts","sourceRoot":"","sources":["../../../../../src/ui/components/icons.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAoB,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhE,UAAU,SAAS;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AAED,wBAAwB;AACxB,wBAAgB,SAAS,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAcnF;AAED,KAAK,gBAAgB,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,CAAC;AASzD,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,EAC1B,IAAS,EACT,KAAyB,EACzB,SAAkB,EAClB,KAAK,GACN,EAAE,SAAS,GAAG;IAAE,SAAS,CAAC,EAAE,gBAAgB,CAAA;CAAE,+BAgB9C;AAED,kFAAkF;AAClF,wBAAgB,QAAQ,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAmB9E;AAED,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAQ/E;AAED,2EAA2E;AAC3E,wBAAgB,cAAc,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA2BxF;AAED,mDAAmD;AACnD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA2BjF;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA4B9E;AAED,+DAA+D;AAC/D,wBAAgB,eAAe,CAAC,EAAE,IAAS,EAAE,KAA0B,EAAE,KAAK,EAAE,EAAE,SAAS,+BA6B1F;AAED,oCAAoC;AACpC,wBAAgB,QAAQ,CAAC,EAAE,IAAS,EAAE,KAA0B,EAAE,KAAK,EAAE,EAAE,SAAS,+BA6BnF;AAED,6DAA6D;AAC7D,wBAAgB,UAAU,CAAC,EAAE,IAAS,EAAE,KAAqB,EAAE,KAAK,EAAE,EAAE,SAAS,+BA8BhF;AAED,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,EAAE,IAAS,EAAE,KAA2B,EAAE,KAAK,EAAE,EAAE,SAAS,+BA+BrF;AAED,wBAAgB,aAAa,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAgBvF;AASD,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,EAAE,IAAS,EAAE,KAAyB,EAAE,KAAK,EAAE,EAAE,SAAS,+BAkCnF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0DH,OAAO,KAAK,EAAc,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAElE,wBAAgB,QAAQ,CAAC,EACvB,QAAQ,EACR,OAAO,EACP,eAAe,EACf,WAAW,EACX,KAAK,EACL,SAAS,EACT,WAAW,EACX,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,WAAgB,EAChB,cAAc,EACd,MAAM,EACN,WAAW,EACX,cAAc,EACd,MAAM,EACN,iBAAsB,EACtB,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,aAAa,EACb,UAAU,EACV,QAAQ,EACR,MAAM,EACN,OAAO,GACR,EAAE,aAAa,+BAuiBf"}
|
|
@@ -207,15 +207,6 @@ export declare const styles: {
|
|
|
207
207
|
height: number;
|
|
208
208
|
borderRadius: 10;
|
|
209
209
|
};
|
|
210
|
-
attachmentFile: {
|
|
211
|
-
borderWidth: number;
|
|
212
|
-
borderRadius: 10;
|
|
213
|
-
paddingHorizontal: number;
|
|
214
|
-
paddingVertical: number;
|
|
215
|
-
};
|
|
216
|
-
attachmentName: {
|
|
217
|
-
fontSize: 13;
|
|
218
|
-
};
|
|
219
210
|
failedText: {
|
|
220
211
|
fontSize: 12;
|
|
221
212
|
marginTop: 4;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.styles.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.styles.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAOlF,eAAO,MAAM,MAAM
|
|
1
|
+
{"version":3,"file":"ChatView.styles.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.styles.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAOlF,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0ZjB,CAAC"}
|
|
@@ -5,6 +5,7 @@ import type { AgentUiAction, AttachmentSource, ConversationRatingResult, Message
|
|
|
5
5
|
import type { AgentUiBlockLabels } from '../../components/AgentUiBlocks';
|
|
6
6
|
import type { ImageViewerLabels } from '../../components/ImageViewer';
|
|
7
7
|
import type { RatingCardLabels } from '../../components/RatingCard';
|
|
8
|
+
import type { VoiceNoteLabels } from '../../components/VoiceNote';
|
|
8
9
|
export interface ChatLabels {
|
|
9
10
|
back: string;
|
|
10
11
|
loading: string;
|
|
@@ -41,6 +42,9 @@ export interface ChatLabels {
|
|
|
41
42
|
rating: RatingCardLabels;
|
|
42
43
|
attach: AttachLabels;
|
|
43
44
|
viewer: ImageViewerLabels;
|
|
45
|
+
voice: VoiceNoteLabels;
|
|
46
|
+
/** Screen-reader label for a file card; keeps `{name}`. */
|
|
47
|
+
openAttachment: string;
|
|
44
48
|
}
|
|
45
49
|
/** Composer attachment copy. Limits are already filled in; `remove` keeps `{name}`. */
|
|
46
50
|
export interface AttachLabels {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,kDAAkD;AAElD,OAAO,KAAK,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,KAAK,EACV,aAAa,EACb,gBAAgB,EAChB,wBAAwB,EACxB,OAAO,EACP,kBAAkB,EACnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,gCAAgC,CAAC;AACzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"ChatView.types.d.ts","sourceRoot":"","sources":["../../../../../../src/ui/screens/chat/ChatView.types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repzo/widget-react-native",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "Repzo chat widget for React Native \u2014 pure JavaScript, no native modules",
|
|
5
5
|
"source": "./src/index.tsx",
|
|
6
6
|
"main": "./lib/commonjs/index.js",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@react-native-async-storage/async-storage": "*",
|
|
55
55
|
"@react-native-firebase/messaging": "*",
|
|
56
|
+
"expo-audio": "*",
|
|
56
57
|
"expo-crypto": "*",
|
|
57
58
|
"expo-document-picker": "*",
|
|
58
59
|
"expo-file-system": "*",
|
|
@@ -70,6 +71,9 @@
|
|
|
70
71
|
"@react-native-firebase/messaging": {
|
|
71
72
|
"optional": true
|
|
72
73
|
},
|
|
74
|
+
"expo-audio": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
73
77
|
"expo-crypto": {
|
|
74
78
|
"optional": true
|
|
75
79
|
},
|
package/src/api/client.ts
CHANGED
|
@@ -62,6 +62,16 @@ export class RepzoWidgetError extends Error {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/** A Send message part asked for on its own, which may share one request with others. */
|
|
66
|
+
type SendPart = 'post' | 'outbound';
|
|
67
|
+
|
|
68
|
+
interface SendPartAsk {
|
|
69
|
+
part: SendPart;
|
|
70
|
+
own: () => Promise<unknown>;
|
|
71
|
+
resolve: (value: unknown) => void;
|
|
72
|
+
reject: (error: unknown) => void;
|
|
73
|
+
}
|
|
74
|
+
|
|
65
75
|
interface RequestOptions {
|
|
66
76
|
method?: 'GET' | 'POST' | 'DELETE';
|
|
67
77
|
body?: unknown;
|
|
@@ -92,6 +102,9 @@ export class WidgetApiClient {
|
|
|
92
102
|
/** Set by the store: a 403 with a terminal `code` came back from any endpoint. */
|
|
93
103
|
onTerminalError: ((error: RepzoWidgetError) => void) | null = null;
|
|
94
104
|
|
|
105
|
+
/** Send parts asked for in this tick — see askSendPart. */
|
|
106
|
+
private sendPartBatch: SendPartAsk[] | null = null;
|
|
107
|
+
|
|
95
108
|
configure(baseUrl: string, channelAccountId: string) {
|
|
96
109
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
97
110
|
this.channelAccountId = channelAccountId;
|
|
@@ -194,7 +207,9 @@ export class WidgetApiClient {
|
|
|
194
207
|
}
|
|
195
208
|
|
|
196
209
|
getOutboundMessage(): Promise<{ message: OutboundMessage | null }> {
|
|
197
|
-
return this.
|
|
210
|
+
return this.askSendPart('outbound', () =>
|
|
211
|
+
this.request(`/api/widget/outbound/${encodeURIComponent(this.channelAccountId)}`)
|
|
212
|
+
);
|
|
198
213
|
}
|
|
199
214
|
|
|
200
215
|
/**
|
|
@@ -202,7 +217,46 @@ export class WidgetApiClient {
|
|
|
202
217
|
* `recheckInMs`: ask again after this long — the shown one stops then.
|
|
203
218
|
*/
|
|
204
219
|
getPost(): Promise<{ post: WidgetPost | null; recheckInMs?: number | null }> {
|
|
205
|
-
return this.request('/api/widget/send/post?platform=mobile');
|
|
220
|
+
return this.askSendPart('post', () => this.request('/api/widget/send/post?platform=mobile'));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The post and the launcher's chat message are both asked for right after a
|
|
225
|
+
* session starts: asks made in the same tick go out as one request (each
|
|
226
|
+
* part answers the way its own endpoint does). A lone ask, a Workstation
|
|
227
|
+
* deployment that doesn't know a part yet (it leaves it out — the
|
|
228
|
+
* `outbound` part is newer than the combined endpoint), or a failed combined
|
|
229
|
+
* request falls back to each part's own endpoint, as before.
|
|
230
|
+
*/
|
|
231
|
+
private askSendPart<T>(part: SendPart, own: () => Promise<T>): Promise<T> {
|
|
232
|
+
return new Promise<T>((resolve, reject) => {
|
|
233
|
+
if (!this.sendPartBatch) {
|
|
234
|
+
this.sendPartBatch = [];
|
|
235
|
+
setTimeout(() => void this.flushSendParts(), 0);
|
|
236
|
+
}
|
|
237
|
+
this.sendPartBatch.push({ part, own, resolve: resolve as (value: unknown) => void, reject });
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async flushSendParts(): Promise<void> {
|
|
242
|
+
const asks = this.sendPartBatch ?? [];
|
|
243
|
+
this.sendPartBatch = null;
|
|
244
|
+
const parts = Array.from(new Set(asks.map((ask) => ask.part)));
|
|
245
|
+
let answer: Record<string, unknown> | null = null;
|
|
246
|
+
if (parts.length > 1) {
|
|
247
|
+
try {
|
|
248
|
+
answer = await this.request<Record<string, unknown>>(
|
|
249
|
+
`/api/widget/send/messages?include=${parts.join(',')}&platform=mobile`
|
|
250
|
+
);
|
|
251
|
+
} catch {
|
|
252
|
+
answer = null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const ask of asks) {
|
|
256
|
+
const value = answer?.[ask.part];
|
|
257
|
+
if (value && typeof value === 'object') ask.resolve(value);
|
|
258
|
+
else ask.own().then(ask.resolve, ask.reject);
|
|
259
|
+
}
|
|
206
260
|
}
|
|
207
261
|
|
|
208
262
|
/**
|
|
@@ -367,6 +421,11 @@ export class WidgetApiClient {
|
|
|
367
421
|
return this.request('/api/widget/push-token', { method: 'POST', body: data });
|
|
368
422
|
}
|
|
369
423
|
|
|
424
|
+
/** A Send push (`document_type: 'send_push'`) was tapped — counts as an open in Workstation. */
|
|
425
|
+
reportPushOpened(deliveryId: string): Promise<void> {
|
|
426
|
+
return this.request('/api/widget/push-opened', { method: 'POST', body: { deliveryId } });
|
|
427
|
+
}
|
|
428
|
+
|
|
370
429
|
/** Logout passes the session's token explicitly and `skipReauth`. */
|
|
371
430
|
removePushToken(
|
|
372
431
|
token: string,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Voice notes (and any audio file) play inside the chat when the host app has
|
|
3
|
+
* `expo-audio`; without it they stay a file row that opens in the browser.
|
|
4
|
+
*
|
|
5
|
+
* Resolved once at module load, like the attachment pickers: each `require` is
|
|
6
|
+
* static so Metro can see it, and with `allowOptionalDependencies` (see
|
|
7
|
+
* metro.js) an uninstalled one bundles as a require that throws, which the
|
|
8
|
+
* `try` around it catches.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
let expoAudio: any = null;
|
|
12
|
+
try {
|
|
13
|
+
const mod = require('expo-audio');
|
|
14
|
+
if (typeof mod?.useAudioPlayer === 'function' && typeof mod?.useAudioPlayerStatus === 'function') {
|
|
15
|
+
expoAudio = mod;
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
expoAudio = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// expo-audio 1.x (Expo SDK 54+) takes `{ updateInterval }`; 0.x (SDK 53) took
|
|
22
|
+
// the interval itself. Passing the wrong shape would break the player.
|
|
23
|
+
let takesOptionsObject = true;
|
|
24
|
+
try {
|
|
25
|
+
const version: unknown = require('expo-audio/package.json')?.version;
|
|
26
|
+
if (typeof version === 'string') takesOptionsObject = Number(version.split('.')[0]) >= 1;
|
|
27
|
+
} catch {
|
|
28
|
+
takesOptionsObject = true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** How often the playhead moves while a note plays. */
|
|
32
|
+
const UPDATE_INTERVAL_MS = 100;
|
|
33
|
+
|
|
34
|
+
/** The installed expo-audio module, or null when the app doesn't have it. */
|
|
35
|
+
export function voicePlaybackModule(): any {
|
|
36
|
+
return expoAudio;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The second argument to `useAudioPlayer` for the installed expo-audio. */
|
|
40
|
+
export function audioPlayerOptions(): unknown {
|
|
41
|
+
return takesOptionsObject ? { updateInterval: UPDATE_INTERVAL_MS } : UPDATE_INTERVAL_MS;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Audio the chat can play in place: an `audio/*` attachment with a web address. */
|
|
45
|
+
export function isPlayableAudio(contentType: string | null | undefined, url: string | null | undefined): boolean {
|
|
46
|
+
return !!expoAudio && (contentType ?? '').toLowerCase().startsWith('audio/') && /^https?:\/\//i.test(url ?? '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── One note at a time ─────────────────────────────────────────────────────
|
|
50
|
+
// Each bubble owns its own player, so starting a second note would otherwise
|
|
51
|
+
// talk over the first.
|
|
52
|
+
|
|
53
|
+
let playing: { pause: () => void } | null = null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Called as a note starts: pauses whichever note was playing and returns a
|
|
57
|
+
* release for this one's claim (call it when the note goes away).
|
|
58
|
+
*/
|
|
59
|
+
export function claimVoicePlayback(pause: () => void): () => void {
|
|
60
|
+
const claim = { pause };
|
|
61
|
+
const previous = playing;
|
|
62
|
+
playing = claim;
|
|
63
|
+
if (previous) {
|
|
64
|
+
try {
|
|
65
|
+
previous.pause();
|
|
66
|
+
} catch {
|
|
67
|
+
// Its player was released with its bubble: nothing is playing.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return () => {
|
|
71
|
+
if (playing === claim) playing = null;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Waveform ───────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Bar heights (0.15–1) for a note's waveform. Workstation doesn't send the
|
|
79
|
+
* recorded loudness to visitors, so the pattern is made up — seeded from the
|
|
80
|
+
* attachment, so the same note always draws the same bars.
|
|
81
|
+
*/
|
|
82
|
+
export function voiceWaveformLevels(seed: string, count: number): number[] {
|
|
83
|
+
let h = 2166136261;
|
|
84
|
+
for (let i = 0; i < seed.length; i += 1) {
|
|
85
|
+
h ^= seed.charCodeAt(i);
|
|
86
|
+
h = Math.imul(h, 16777619);
|
|
87
|
+
}
|
|
88
|
+
const levels: number[] = [];
|
|
89
|
+
for (let i = 0; i < count; i += 1) {
|
|
90
|
+
h ^= h << 13;
|
|
91
|
+
h ^= h >>> 17;
|
|
92
|
+
h ^= h << 5;
|
|
93
|
+
const n = ((h >>> 0) % 1000) / 1000;
|
|
94
|
+
levels.push(0.15 + Math.pow(n, 0.85) * 0.85);
|
|
95
|
+
}
|
|
96
|
+
return levels;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `m:ss` for a playback position or length. */
|
|
100
|
+
export function formatVoiceClock(seconds: number): string {
|
|
101
|
+
const s = Math.max(0, Math.floor(Number.isFinite(seconds) ? seconds : 0));
|
|
102
|
+
return `${Math.floor(s / 60)}:${(s % 60).toString().padStart(2, '0')}`;
|
|
103
|
+
}
|
package/src/i18n.ts
CHANGED
|
@@ -70,6 +70,12 @@ export const DEFAULT_LABELS = {
|
|
|
70
70
|
attachmentsTooLargeTogether:
|
|
71
71
|
'Attachments are too large to send together — send some in another message',
|
|
72
72
|
attachmentFailed: "Couldn't attach that file",
|
|
73
|
+
// A file in a message; tapping opens it. `{name}` is the file name.
|
|
74
|
+
openAttachment: 'Open {name}',
|
|
75
|
+
// Voice notes played in the chat (with expo-audio).
|
|
76
|
+
voicePlay: 'Play voice message',
|
|
77
|
+
voicePause: 'Pause voice message',
|
|
78
|
+
voiceSpeed: 'Playback speed',
|
|
73
79
|
// Full-screen image viewer.
|
|
74
80
|
imageLoadFailed: "Couldn't load this image",
|
|
75
81
|
openImageExternally: 'Open image',
|
package/src/index.tsx
CHANGED
|
@@ -156,6 +156,17 @@ export type RepzoWidgetType = {
|
|
|
156
156
|
* installed, `boot({ push: { enabled: true } })` handles everything).
|
|
157
157
|
*/
|
|
158
158
|
setPushToken(options: { token: string; platform?: 'ios' | 'android'; bundleId?: string }): Promise<void>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Hand over a tapped notification your app read itself (Firebase's
|
|
162
|
+
* `getInitialNotification` / `onNotificationOpenedApp`, or a notification
|
|
163
|
+
* you re-posted locally). Returns true when it's a Repzo Widget push — it
|
|
164
|
+
* opens (now, or once the session is ready) and your app should skip its
|
|
165
|
+
* own routing; false for your app's own pushes.
|
|
166
|
+
*
|
|
167
|
+
* @since 1.2.1
|
|
168
|
+
*/
|
|
169
|
+
handlePushNotification(message: { messageId?: string; data?: Record<string, unknown> }): boolean;
|
|
159
170
|
};
|
|
160
171
|
|
|
161
172
|
const RepzoWidget: RepzoWidgetType = {
|
|
@@ -202,6 +213,8 @@ const RepzoWidget: RepzoWidgetType = {
|
|
|
202
213
|
setSecureStorageAdapter: (adapter) => setSecureStorageAdapter(adapter),
|
|
203
214
|
|
|
204
215
|
setPushToken: (options) => controller.store.push.setManualToken(options),
|
|
216
|
+
|
|
217
|
+
handlePushNotification: (message) => controller.store.push.handleNotification(message),
|
|
205
218
|
};
|
|
206
219
|
|
|
207
220
|
export default RepzoWidget;
|
package/src/locales.ts
CHANGED
|
@@ -29,6 +29,10 @@ const ar: LabelMap = {
|
|
|
29
29
|
attachmentTooMany: 'يمكنك إرفاق {count} ملفات كحد أقصى',
|
|
30
30
|
attachmentsTooLargeTogether: 'المرفقات كبيرة جدًا لإرسالها معًا — أرسل بعضها في رسالة أخرى',
|
|
31
31
|
attachmentFailed: 'تعذر إرفاق هذا الملف',
|
|
32
|
+
openAttachment: 'فتح {name}',
|
|
33
|
+
voicePlay: 'تشغيل الرسالة الصوتية',
|
|
34
|
+
voicePause: 'إيقاف الرسالة الصوتية مؤقتًا',
|
|
35
|
+
voiceSpeed: 'سرعة التشغيل',
|
|
32
36
|
imageLoadFailed: 'تعذر تحميل هذه الصورة',
|
|
33
37
|
openImageExternally: 'فتح الصورة',
|
|
34
38
|
cameraPermissionDenied: 'اسمح بالوصول إلى الكاميرا من الإعدادات لالتقاط صورة',
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { Platform } from 'react-native';
|
|
17
17
|
import type { WidgetApiClient } from '../api/client';
|
|
18
|
+
import { openExternalUrl } from '../ui/utils/safe-url';
|
|
18
19
|
|
|
19
20
|
export interface WidgetPushOptions {
|
|
20
21
|
/** Register this device for FCM pushes after every successful session init. */
|
|
@@ -90,6 +91,18 @@ function resolveMessaging(): MessagingHandle | null {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
const WIDGET_DOCUMENT_TYPES = new Set(['widget_message', 'send_post', 'send_push']);
|
|
95
|
+
|
|
96
|
+
/** A push sent by Workstation for the widget (vs. one of the host app's own). */
|
|
97
|
+
function isWidgetPush(data: any): data is Record<string, unknown> & { document_id: string } {
|
|
98
|
+
return (
|
|
99
|
+
!!data &&
|
|
100
|
+
WIDGET_DOCUMENT_TYPES.has(data.document_type) &&
|
|
101
|
+
typeof data.document_id === 'string' &&
|
|
102
|
+
!!data.document_id
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
export class PushManager {
|
|
94
107
|
private options: WidgetPushOptions = {};
|
|
95
108
|
|
|
@@ -107,6 +120,12 @@ export class PushManager {
|
|
|
107
120
|
/** Registrations still on the wire; unregister() waits for them. */
|
|
108
121
|
private inflight = new Set<Promise<void>>();
|
|
109
122
|
|
|
123
|
+
/** Taps the host handed over before the session was ready; start() opens them. */
|
|
124
|
+
private pendingTaps: any[] = [];
|
|
125
|
+
|
|
126
|
+
/** The host and our own listener can both see one tap — open it once. */
|
|
127
|
+
private handledMessageIds = new Set<string>();
|
|
128
|
+
|
|
110
129
|
constructor(
|
|
111
130
|
private client: WidgetApiClient,
|
|
112
131
|
private openConversation: (conversationId: string) => void,
|
|
@@ -125,6 +144,9 @@ export class PushManager {
|
|
|
125
144
|
*/
|
|
126
145
|
async start(): Promise<void> {
|
|
127
146
|
this.active = true;
|
|
147
|
+
const pending = this.pendingTaps;
|
|
148
|
+
this.pendingTaps = [];
|
|
149
|
+
pending.forEach((message) => this.handleTap(message));
|
|
128
150
|
if (this.manualToken) {
|
|
129
151
|
await this.register(this.manualToken);
|
|
130
152
|
}
|
|
@@ -193,6 +215,8 @@ export class PushManager {
|
|
|
193
215
|
*/
|
|
194
216
|
async unregister(authToken: string | null): Promise<void> {
|
|
195
217
|
this.active = false;
|
|
218
|
+
// A tap held for this session must not open in the next user's.
|
|
219
|
+
this.pendingTaps = [];
|
|
196
220
|
await Promise.all([...this.inflight]);
|
|
197
221
|
const token = this.lastToken;
|
|
198
222
|
this.lastToken = null;
|
|
@@ -228,29 +252,68 @@ export class PushManager {
|
|
|
228
252
|
}
|
|
229
253
|
|
|
230
254
|
private wireTapHandlers(messaging: MessagingHandle) {
|
|
255
|
+
// These listeners outlive logout, so taps go through the same session gate
|
|
256
|
+
// as host-forwarded ones: held until start(), and only marked handled then.
|
|
231
257
|
// Background → tapped: the app was alive, just not in front.
|
|
232
258
|
messaging.onNotificationOpenedApp((remoteMessage: any) => {
|
|
233
|
-
this.
|
|
259
|
+
this.handleNotification(remoteMessage);
|
|
234
260
|
});
|
|
235
261
|
|
|
236
262
|
// Killed → tapped: the click that launched the app.
|
|
237
263
|
void messaging
|
|
238
264
|
.getInitialNotification()
|
|
239
265
|
.then((remoteMessage: any) => {
|
|
240
|
-
if (remoteMessage) this.
|
|
266
|
+
if (remoteMessage) this.handleNotification(remoteMessage);
|
|
241
267
|
})
|
|
242
268
|
.catch(() => {});
|
|
243
269
|
}
|
|
244
270
|
|
|
271
|
+
/**
|
|
272
|
+
* A tap the host app read itself (`RepzoWidget.handlePushNotification`).
|
|
273
|
+
* Firebase hands out the notification that launched the app only once, so
|
|
274
|
+
* when the host reads it first our own listener gets nothing; the same goes
|
|
275
|
+
* for notifications the host re-posts locally while in the foreground.
|
|
276
|
+
* True when it's a widget push (so the host skips its own routing); it opens
|
|
277
|
+
* now, or once the session is ready.
|
|
278
|
+
*/
|
|
279
|
+
handleNotification(message: any): boolean {
|
|
280
|
+
if (!isWidgetPush(message?.data)) return false;
|
|
281
|
+
if (this.active) this.handleTap(message);
|
|
282
|
+
else this.pendingTaps.push(message);
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
|
|
245
286
|
/**
|
|
246
287
|
* Only widget pushes are ours — host app notifications pass through. A team
|
|
247
|
-
* reply opens its conversation; a Send post opens the post
|
|
288
|
+
* reply opens its conversation; a Send post opens the post; a Send mobile
|
|
289
|
+
* push reports the open and follows its link, if it has one.
|
|
248
290
|
*/
|
|
249
291
|
private handleTap(remoteMessage: any) {
|
|
250
292
|
const data = remoteMessage?.data;
|
|
251
|
-
|
|
252
|
-
|
|
293
|
+
if (!isWidgetPush(data)) return;
|
|
294
|
+
const messageId = remoteMessage?.messageId;
|
|
295
|
+
if (typeof messageId === 'string' && messageId) {
|
|
296
|
+
if (this.handledMessageIds.has(messageId)) return;
|
|
297
|
+
this.handledMessageIds.add(messageId);
|
|
298
|
+
}
|
|
299
|
+
const documentId = data.document_id;
|
|
253
300
|
if (data?.document_type === 'widget_message') this.openConversation(documentId);
|
|
254
301
|
else if (data?.document_type === 'send_post') this.openPost(documentId);
|
|
302
|
+
else if (data?.document_type === 'send_push') this.openSendPush(data);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The tap already opened the app. A link goes through the same gate as links
|
|
307
|
+
* in messages (`onOpenUrl` first, then web/mail/phone links), so the host can
|
|
308
|
+
* route `myapp://…` itself.
|
|
309
|
+
*/
|
|
310
|
+
private openSendPush(data: Record<string, unknown>) {
|
|
311
|
+
const deliveryId = data.delivery_id;
|
|
312
|
+
if (typeof deliveryId === 'string' && deliveryId) {
|
|
313
|
+
this.client.reportPushOpened(deliveryId).catch(() => {
|
|
314
|
+
// Only the open count misses it; the visitor sees nothing.
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (typeof data.url === 'string' && data.url) void openExternalUrl(data.url);
|
|
255
318
|
}
|
|
256
319
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* component is mounted; this hook only subscribes to it.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { useSyncExternalStore } from 'react';
|
|
7
|
+
import { useRef, useSyncExternalStore } from 'react';
|
|
8
8
|
|
|
9
9
|
import { controller } from '../controller';
|
|
10
10
|
import type { WidgetState } from '../types';
|
|
@@ -18,3 +18,32 @@ export function useWidgetState(): WidgetState {
|
|
|
18
18
|
export function useWidgetStore(): WidgetStore {
|
|
19
19
|
return controller.store;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A slice of the state that re-renders only when the slice changes (by
|
|
24
|
+
* `isEqual`) — for UI that lives on the host's own screens, like the launcher,
|
|
25
|
+
* which shouldn't redraw on every message or typing event. Internal.
|
|
26
|
+
*/
|
|
27
|
+
export function useWidgetSelector<T>(
|
|
28
|
+
select: (state: WidgetState) => T,
|
|
29
|
+
isEqual: (previous: T, next: T) => boolean
|
|
30
|
+
): T {
|
|
31
|
+
const store = controller.store;
|
|
32
|
+
const last = useRef<{ state: WidgetState; value: T } | null>(null);
|
|
33
|
+
const getSnapshot = () => {
|
|
34
|
+
const state = store.getState();
|
|
35
|
+
const previous = last.current;
|
|
36
|
+
if (previous && previous.state === state) return previous.value;
|
|
37
|
+
const value = select(state);
|
|
38
|
+
// Same slice from a newer state: keep the old value so React bails out.
|
|
39
|
+
last.current = { state, value: previous && isEqual(previous.value, value) ? previous.value : value };
|
|
40
|
+
return last.current.value;
|
|
41
|
+
};
|
|
42
|
+
return useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Same keys, same values (by identity). */
|
|
46
|
+
export function shallowEqual<T extends Record<string, unknown>>(previous: T, next: T): boolean {
|
|
47
|
+
const keys = Object.keys(previous);
|
|
48
|
+
return keys.length === Object.keys(next).length && keys.every((key) => Object.is(previous[key], next[key]));
|
|
49
|
+
}
|
|
@@ -40,7 +40,7 @@ import type {
|
|
|
40
40
|
import { setOpenUrlHandler } from '../ui/utils/safe-url';
|
|
41
41
|
|
|
42
42
|
/** Sent on every init; must match `package.json` (tests/version.test.js checks). */
|
|
43
|
-
const SDK_VERSION = '@repzo/widget-react-native@1.2.
|
|
43
|
+
const SDK_VERSION = '@repzo/widget-react-native@1.2.2';
|
|
44
44
|
|
|
45
45
|
/** The platform this session reports — mobile sessions get the Mobile SDKs overrides. */
|
|
46
46
|
function sessionPlatform(): 'ios' | 'android' | 'web' {
|
|
@@ -67,6 +67,13 @@ const LOGOUT_REQUEST_TIMEOUT_MS = 5_000;
|
|
|
67
67
|
const STREAM_REAUTH_MIN_INTERVAL_MS = 30_000;
|
|
68
68
|
/** Live replies in the open thread advance the server read cursor, batched. */
|
|
69
69
|
const MARK_READ_DEBOUNCE_MS = 1_000;
|
|
70
|
+
/**
|
|
71
|
+
* Back from the background sooner than this, availability and the post are
|
|
72
|
+
* still current enough to skip re-asking. Reps hop to maps or the camera and
|
|
73
|
+
* back all day; the list and the open thread still catch up every time, since
|
|
74
|
+
* the stream was closed meanwhile.
|
|
75
|
+
*/
|
|
76
|
+
const FOREGROUND_REFRESH_AFTER_MS = 5 * 60_000;
|
|
70
77
|
/** Live events held back while a draft's first send is creating its thread. */
|
|
71
78
|
const DRAFT_EVENT_BUFFER_LIMIT = 50;
|
|
72
79
|
/** How long a post stays up after a reaction, thanking them, before it goes. */
|
|
@@ -211,6 +218,9 @@ export class WidgetStore {
|
|
|
211
218
|
|
|
212
219
|
private appStateSubscription: { remove(): void } | null = null;
|
|
213
220
|
|
|
221
|
+
/** When the app last went to the background (0: hasn't this session). */
|
|
222
|
+
private backgroundedAt = 0;
|
|
223
|
+
|
|
214
224
|
private lastTypingPingAt = 0;
|
|
215
225
|
|
|
216
226
|
private visitorTypingActive = false;
|
|
@@ -914,10 +924,14 @@ export class WidgetStore {
|
|
|
914
924
|
|
|
915
925
|
private watchAppState() {
|
|
916
926
|
if (this.appStateSubscription) return;
|
|
927
|
+
// Booted while already in the background (a push woke the app): coming
|
|
928
|
+
// to the foreground is a return from the background too.
|
|
929
|
+
if (AppState.currentState === 'background') this.backgroundedAt = Date.now();
|
|
917
930
|
this.appStateSubscription = AppState.addEventListener('change', (status: AppStateStatus) => {
|
|
918
931
|
if (status === 'active') {
|
|
919
932
|
void this.resumeFromBackground();
|
|
920
933
|
} else if (status === 'background') {
|
|
934
|
+
this.backgroundedAt = Date.now();
|
|
921
935
|
this.realtime.close();
|
|
922
936
|
}
|
|
923
937
|
});
|
|
@@ -931,6 +945,12 @@ export class WidgetStore {
|
|
|
931
945
|
if (this.state.error) await this.retry();
|
|
932
946
|
return;
|
|
933
947
|
}
|
|
948
|
+
// Only the background closes the stream. Back from merely inactive (a
|
|
949
|
+
// system sheet, Face ID, the notification shade) it never stopped, so
|
|
950
|
+
// there is nothing to catch up on.
|
|
951
|
+
if (!this.backgroundedAt) return;
|
|
952
|
+
const awayMs = Date.now() - this.backgroundedAt;
|
|
953
|
+
this.backgroundedAt = 0;
|
|
934
954
|
const epoch = this.epoch;
|
|
935
955
|
const token = await this.client.getToken();
|
|
936
956
|
if (!token || this.isStale(epoch)) return;
|
|
@@ -938,6 +958,7 @@ export class WidgetStore {
|
|
|
938
958
|
// The stream had a gap: refresh the list so unread/snippets are correct,
|
|
939
959
|
// and the thread on screen so replies sent meanwhile show up.
|
|
940
960
|
await Promise.all([this.refreshConversations(), this.refreshOpenThread()]);
|
|
961
|
+
if (awayMs < FOREGROUND_REFRESH_AFTER_MS) return;
|
|
941
962
|
await this.refreshConfig();
|
|
942
963
|
// A post may have gone live (or ended) while the app was away.
|
|
943
964
|
if (!this.isStale(epoch)) this.loadPost();
|