@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.
Files changed (135) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/README.md +73 -1
  3. package/lib/commonjs/api/client.js +57 -2
  4. package/lib/commonjs/api/client.js.map +1 -1
  5. package/lib/commonjs/audio/voice.js +119 -0
  6. package/lib/commonjs/audio/voice.js.map +1 -0
  7. package/lib/commonjs/i18n.js +6 -0
  8. package/lib/commonjs/i18n.js.map +1 -1
  9. package/lib/commonjs/index.js +2 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/locales.js +4 -0
  12. package/lib/commonjs/locales.js.map +1 -1
  13. package/lib/commonjs/push/push-notifications.js +61 -6
  14. package/lib/commonjs/push/push-notifications.js.map +1 -1
  15. package/lib/commonjs/state/use-widget-state.js +31 -0
  16. package/lib/commonjs/state/use-widget-state.js.map +1 -1
  17. package/lib/commonjs/state/widget-store.js +22 -1
  18. package/lib/commonjs/state/widget-store.js.map +1 -1
  19. package/lib/commonjs/ui/WidgetProvider.js +6 -0
  20. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  21. package/lib/commonjs/ui/components/FileCard.js +134 -0
  22. package/lib/commonjs/ui/components/FileCard.js.map +1 -0
  23. package/lib/commonjs/ui/components/Launcher.js +15 -1
  24. package/lib/commonjs/ui/components/Launcher.js.map +1 -1
  25. package/lib/commonjs/ui/components/VoiceNote.js +354 -0
  26. package/lib/commonjs/ui/components/VoiceNote.js.map +1 -0
  27. package/lib/commonjs/ui/components/icons.js +57 -0
  28. package/lib/commonjs/ui/components/icons.js.map +1 -1
  29. package/lib/commonjs/ui/screens/chat/ChatView.js +33 -14
  30. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  31. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +0 -9
  32. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
  33. package/lib/module/api/client.js +58 -2
  34. package/lib/module/api/client.js.map +1 -1
  35. package/lib/module/audio/voice.js +110 -0
  36. package/lib/module/audio/voice.js.map +1 -0
  37. package/lib/module/i18n.js +6 -0
  38. package/lib/module/i18n.js.map +1 -1
  39. package/lib/module/index.js +2 -1
  40. package/lib/module/index.js.map +1 -1
  41. package/lib/module/locales.js +4 -0
  42. package/lib/module/locales.js.map +1 -1
  43. package/lib/module/push/push-notifications.js +61 -6
  44. package/lib/module/push/push-notifications.js.map +1 -1
  45. package/lib/module/state/use-widget-state.js +30 -1
  46. package/lib/module/state/use-widget-state.js.map +1 -1
  47. package/lib/module/state/widget-store.js +22 -1
  48. package/lib/module/state/widget-store.js.map +1 -1
  49. package/lib/module/ui/WidgetProvider.js +6 -0
  50. package/lib/module/ui/WidgetProvider.js.map +1 -1
  51. package/lib/module/ui/components/FileCard.js +130 -0
  52. package/lib/module/ui/components/FileCard.js.map +1 -0
  53. package/lib/module/ui/components/Launcher.js +16 -2
  54. package/lib/module/ui/components/Launcher.js.map +1 -1
  55. package/lib/module/ui/components/VoiceNote.js +350 -0
  56. package/lib/module/ui/components/VoiceNote.js.map +1 -0
  57. package/lib/module/ui/components/icons.js +55 -0
  58. package/lib/module/ui/components/icons.js.map +1 -1
  59. package/lib/module/ui/screens/chat/ChatView.js +33 -14
  60. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  61. package/lib/module/ui/screens/chat/ChatView.styles.js +0 -9
  62. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
  63. package/lib/typescript/commonjs/api/client.d.ts +14 -0
  64. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/audio/voice.d.ts +29 -0
  66. package/lib/typescript/commonjs/audio/voice.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/i18n.d.ts +4 -0
  68. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/index.d.ts +13 -0
  70. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/push/push-notifications.d.ts +21 -1
  73. package/lib/typescript/commonjs/push/push-notifications.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/state/use-widget-state.d.ts +8 -0
  75. package/lib/typescript/commonjs/state/use-widget-state.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/state/widget-store.d.ts +2 -0
  77. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/components/FileCard.d.ts +21 -0
  80. package/lib/typescript/commonjs/ui/components/FileCard.d.ts.map +1 -0
  81. package/lib/typescript/commonjs/ui/components/Launcher.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts +32 -0
  83. package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/ui/components/icons.d.ts +4 -0
  85. package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +0 -9
  88. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +4 -0
  90. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  91. package/lib/typescript/module/api/client.d.ts +14 -0
  92. package/lib/typescript/module/api/client.d.ts.map +1 -1
  93. package/lib/typescript/module/audio/voice.d.ts +29 -0
  94. package/lib/typescript/module/audio/voice.d.ts.map +1 -0
  95. package/lib/typescript/module/i18n.d.ts +4 -0
  96. package/lib/typescript/module/i18n.d.ts.map +1 -1
  97. package/lib/typescript/module/index.d.ts +13 -0
  98. package/lib/typescript/module/index.d.ts.map +1 -1
  99. package/lib/typescript/module/locales.d.ts.map +1 -1
  100. package/lib/typescript/module/push/push-notifications.d.ts +21 -1
  101. package/lib/typescript/module/push/push-notifications.d.ts.map +1 -1
  102. package/lib/typescript/module/state/use-widget-state.d.ts +8 -0
  103. package/lib/typescript/module/state/use-widget-state.d.ts.map +1 -1
  104. package/lib/typescript/module/state/widget-store.d.ts +2 -0
  105. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  106. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  107. package/lib/typescript/module/ui/components/FileCard.d.ts +21 -0
  108. package/lib/typescript/module/ui/components/FileCard.d.ts.map +1 -0
  109. package/lib/typescript/module/ui/components/Launcher.d.ts.map +1 -1
  110. package/lib/typescript/module/ui/components/VoiceNote.d.ts +32 -0
  111. package/lib/typescript/module/ui/components/VoiceNote.d.ts.map +1 -0
  112. package/lib/typescript/module/ui/components/icons.d.ts +4 -0
  113. package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
  114. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  115. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +0 -9
  116. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  117. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +4 -0
  118. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  119. package/package.json +5 -1
  120. package/src/api/client.ts +61 -2
  121. package/src/audio/voice.ts +103 -0
  122. package/src/i18n.ts +6 -0
  123. package/src/index.tsx +13 -0
  124. package/src/locales.ts +4 -0
  125. package/src/push/push-notifications.ts +68 -5
  126. package/src/state/use-widget-state.ts +30 -1
  127. package/src/state/widget-store.ts +22 -1
  128. package/src/ui/WidgetProvider.tsx +6 -0
  129. package/src/ui/components/FileCard.tsx +111 -0
  130. package/src/ui/components/Launcher.tsx +7 -2
  131. package/src/ui/components/VoiceNote.tsx +363 -0
  132. package/src/ui/components/icons.tsx +33 -0
  133. package/src/ui/screens/chat/ChatView.styles.ts +0 -9
  134. package/src/ui/screens/chat/ChatView.tsx +39 -13
  135. 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;AAuDH,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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmajB,CAAC"}
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":"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"}
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.0",
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.request(`/api/widget/outbound/${encodeURIComponent(this.channelAccountId)}`);
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.handleTap(remoteMessage);
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.handleTap(remoteMessage);
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
- const documentId = data?.document_id;
252
- if (typeof documentId !== 'string' || !documentId) return;
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.0';
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();