@ethora/chat-component-rn 26.5.11 → 26.5.13

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 (154) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -0
  3. package/lib/commonjs/assets/icons.js +7 -1
  4. package/lib/commonjs/assets/icons.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatRoom.js +58 -22
  6. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/MediaMessage.js +288 -5
  8. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/ReduxWrapper.js +24 -15
  10. package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
  11. package/lib/commonjs/components/MessageBubble/MessageInteractions.js +23 -3
  12. package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
  13. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js +5 -44
  14. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
  16. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  17. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +149 -0
  18. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  19. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +4 -17
  20. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  21. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +19 -33
  22. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  23. package/lib/commonjs/components/styled/AudioMessage.js +643 -66
  24. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  25. package/lib/commonjs/components/styled/Button.js +9 -3
  26. package/lib/commonjs/components/styled/Button.js.map +1 -1
  27. package/lib/commonjs/components/styled/SendInput.js +239 -40
  28. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  29. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  30. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  31. package/lib/commonjs/components/styled/UnsupportedType.js +47 -5
  32. package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
  33. package/lib/commonjs/context/xmppProvider.js +68 -2
  34. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  35. package/lib/commonjs/helpers/keyboardLayout.js +5 -3
  36. package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
  37. package/lib/commonjs/helpers/mimeToExtension.js +70 -3
  38. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
  39. package/lib/commonjs/hooks/useEventHandlers.js +1 -1
  40. package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
  41. package/lib/commonjs/hooks/useSendMessage.js +6 -3
  42. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  43. package/lib/commonjs/networking/xmppClient.js +26 -1
  44. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  45. package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js +10 -10
  46. package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js.map +1 -1
  47. package/lib/commonjs/roomStore/roomsSlice.js +65 -4
  48. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  49. package/lib/module/assets/icons.js +7 -1
  50. package/lib/module/assets/icons.js.map +1 -1
  51. package/lib/module/components/MainComponents/ChatRoom.js +59 -23
  52. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  53. package/lib/module/components/MainComponents/MediaMessage.js +290 -7
  54. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  55. package/lib/module/components/MainComponents/ReduxWrapper.js +24 -15
  56. package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
  57. package/lib/module/components/MessageBubble/MessageInteractions.js +24 -4
  58. package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
  59. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js +5 -45
  60. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  61. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
  62. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  63. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +144 -0
  64. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  65. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +3 -17
  66. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  67. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +20 -34
  68. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  69. package/lib/module/components/styled/AudioMessage.js +644 -67
  70. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  71. package/lib/module/components/styled/Button.js +9 -3
  72. package/lib/module/components/styled/Button.js.map +1 -1
  73. package/lib/module/components/styled/SendInput.js +240 -42
  74. package/lib/module/components/styled/SendInput.js.map +1 -1
  75. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  76. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  77. package/lib/module/components/styled/UnsupportedType.js +47 -5
  78. package/lib/module/components/styled/UnsupportedType.js.map +1 -1
  79. package/lib/module/context/xmppProvider.js +70 -4
  80. package/lib/module/context/xmppProvider.js.map +1 -1
  81. package/lib/module/helpers/keyboardLayout.js +4 -2
  82. package/lib/module/helpers/keyboardLayout.js.map +1 -1
  83. package/lib/module/helpers/mimeToExtension.js +67 -3
  84. package/lib/module/helpers/mimeToExtension.js.map +1 -1
  85. package/lib/module/hooks/useEventHandlers.js +1 -1
  86. package/lib/module/hooks/useEventHandlers.js.map +1 -1
  87. package/lib/module/hooks/useSendMessage.js +6 -3
  88. package/lib/module/hooks/useSendMessage.js.map +1 -1
  89. package/lib/module/networking/xmppClient.js +26 -1
  90. package/lib/module/networking/xmppClient.js.map +1 -1
  91. package/lib/module/roomStore/Middleware/unreadMidlleware.js +10 -10
  92. package/lib/module/roomStore/Middleware/unreadMidlleware.js.map +1 -1
  93. package/lib/module/roomStore/roomsSlice.js +63 -3
  94. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  95. package/lib/typescript/assets/icons.d.ts.map +1 -1
  96. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  97. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  98. package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
  99. package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
  100. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts +0 -7
  101. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts.map +1 -1
  102. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  103. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts +8 -0
  104. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts.map +1 -0
  105. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  106. package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
  107. package/lib/typescript/components/styled/AudioMessage.d.ts +8 -2
  108. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  109. package/lib/typescript/components/styled/Button.d.ts.map +1 -1
  110. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  111. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts +5 -1
  112. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts.map +1 -1
  113. package/lib/typescript/components/styled/UnsupportedType.d.ts +5 -0
  114. package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
  115. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  116. package/lib/typescript/helpers/keyboardLayout.d.ts +3 -1
  117. package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
  118. package/lib/typescript/helpers/mimeToExtension.d.ts +31 -1
  119. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
  120. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  121. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  122. package/lib/typescript/roomStore/Middleware/unreadMidlleware.d.ts.map +1 -1
  123. package/lib/typescript/roomStore/roomsSlice.d.ts +18 -1
  124. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  125. package/lib/typescript/types/models/modal.model.d.ts +3 -0
  126. package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
  127. package/lib/typescript/types/types.d.ts +15 -0
  128. package/lib/typescript/types/types.d.ts.map +1 -1
  129. package/package.json +5 -1
  130. package/src/assets/icons.tsx +30 -25
  131. package/src/components/MainComponents/ChatRoom.tsx +61 -23
  132. package/src/components/MainComponents/MediaMessage.tsx +295 -5
  133. package/src/components/MainComponents/ReduxWrapper.tsx +23 -9
  134. package/src/components/MessageBubble/MessageInteractions.tsx +35 -3
  135. package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
  136. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -3
  137. package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
  138. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
  139. package/src/components/RoomComponents/styled/LastAudioMessage.tsx +20 -35
  140. package/src/components/styled/AudioMessage.tsx +724 -59
  141. package/src/components/styled/Button.tsx +20 -11
  142. package/src/components/styled/SendInput.tsx +255 -46
  143. package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
  144. package/src/components/styled/UnsupportedType.tsx +60 -6
  145. package/src/context/xmppProvider.tsx +61 -3
  146. package/src/helpers/keyboardLayout.ts +5 -1
  147. package/src/helpers/mimeToExtension.ts +116 -2
  148. package/src/hooks/useEventHandlers.tsx +1 -1
  149. package/src/hooks/useSendMessage.tsx +12 -6
  150. package/src/networking/xmppClient.ts +24 -1
  151. package/src/roomStore/Middleware/unreadMidlleware.tsx +11 -8
  152. package/src/roomStore/roomsSlice.ts +66 -2
  153. package/src/types/models/modal.model.ts +3 -0
  154. package/src/types/types.ts +32 -0
@@ -7,133 +7,710 @@ exports.default = void 0;
7
7
  var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _expoAv = require("expo-av");
10
+ var FileSystem = _interopRequireWildcard(require("expo-file-system/legacy"));
11
+ var _reactNativeWebview = require("react-native-webview");
12
+ var _icons = require("../../assets/icons");
13
+ var _useChatSettingState = require("../../hooks/useChatSettingState");
14
+ var _mimeToExtension = require("../../helpers/mimeToExtension");
15
+ var _devLogger = require("../../utils/devLogger");
10
16
  var _jsxRuntime = require("react/jsx-runtime");
11
17
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
12
18
  const formatTime = millis => {
13
- const totalSec = Math.floor(millis / 1000);
19
+ const safe = Number.isFinite(millis) && millis > 0 ? millis : 0;
20
+ const totalSec = Math.floor(safe / 1000);
14
21
  const min = Math.floor(totalSec / 60);
15
22
  const sec = totalSec % 60;
16
23
  return `${min}:${sec.toString().padStart(2, '0')}`;
17
24
  };
25
+ // Largest payload we will pull into JS for decoding (input opus) and the
26
+ // largest WAV we will ship back over the bridge (output PCM). Voice
27
+ // messages are a few dozen KB / a couple hundred KB of WAV; these only
28
+ // guard against a pathological multi-minute attachment.
29
+ const MAX_DECODE_BYTES = 25 * 1024 * 1024;
30
+ const hashString = value => {
31
+ let hash = 0;
32
+ for (let i = 0; i < value.length; i += 1) {
33
+ hash = hash * 31 + value.charCodeAt(i) | 0;
34
+ }
35
+ return Math.abs(hash).toString(36);
36
+ };
37
+ const withTimeout = async (promise, ms, label) => {
38
+ let timer = null;
39
+ try {
40
+ return await Promise.race([promise, new Promise((_, reject) => {
41
+ timer = setTimeout(() => reject(new Error(label)), ms);
42
+ })]);
43
+ } finally {
44
+ if (timer) {
45
+ clearTimeout(timer);
46
+ }
47
+ }
48
+ };
49
+ const logAudioDebug = (message, details) => {
50
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
51
+ (0, _devLogger.pushLog)('rn', message, details);
52
+ console.log(`[AudioDebug] ${message}`, details || {});
53
+ }
54
+ };
55
+ const B64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
56
+
57
+ // Decode just the leading bytes of a base64 string into a byte array. Used
58
+ // for magic-number container sniffing — we only need the first ~16 bytes,
59
+ // so this avoids pulling a base64 lib in for a handful of bytes.
60
+ const decodeBase64Head = (b64, maxBytes) => {
61
+ const out = [];
62
+ let buffer = 0;
63
+ let bits = 0;
64
+ for (let i = 0; i < b64.length && out.length < maxBytes; i += 1) {
65
+ const ch = b64[i];
66
+ if (ch === '=') {
67
+ break;
68
+ }
69
+ const idx = B64_ALPHABET.indexOf(ch);
70
+ if (idx === -1) {
71
+ continue;
72
+ }
73
+ buffer = buffer << 6 | idx;
74
+ bits += 6;
75
+ if (bits >= 8) {
76
+ bits -= 8;
77
+ out.push(buffer >> bits & 0xff);
78
+ }
79
+ }
80
+ return out;
81
+ };
82
+
83
+ // Sniff the real container from magic bytes. This is the source of truth —
84
+ // the HTTP mime is frequently `application/octet-stream` and the filename a
85
+ // meaningless `.blob`/`.bin`, so neither can be trusted. Byte signatures:
86
+ // EBML 1A45DFA3 → webm/matroska, OggS → ogg, RIFF…WAVE → wav,
87
+ // ….ftyp → mp4/m4a, ID3 / FFEx → mp3, FFF1/FFF9 → aac(ADTS).
88
+ const detectContainer = bytes => {
89
+ if (bytes.length < 4) {
90
+ return 'unknown';
91
+ }
92
+ if (bytes[0] === 0x1a && bytes[1] === 0x45 && bytes[2] === 0xdf && bytes[3] === 0xa3) {
93
+ return 'webm';
94
+ }
95
+ if (bytes[0] === 0x4f && bytes[1] === 0x67 && bytes[2] === 0x67 && bytes[3] === 0x53) {
96
+ return 'ogg';
97
+ }
98
+ if (bytes[0] === 0x52 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x46 && bytes[8] === 0x57 && bytes[9] === 0x41 && bytes[10] === 0x56 && bytes[11] === 0x45) {
99
+ return 'wav';
100
+ }
101
+ if (bytes[4] === 0x66 && bytes[5] === 0x74 && bytes[6] === 0x79 && bytes[7] === 0x70) {
102
+ return 'mp4';
103
+ }
104
+ if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) {
105
+ return 'mp3';
106
+ }
107
+ if (bytes[0] === 0xff && (bytes[1] & 0xe0) === 0xe0) {
108
+ // MPEG audio frame sync (mp3) or ADTS AAC — both decode natively.
109
+ return bytes[1] === 0xf1 || bytes[1] === 0xf9 ? 'aac' : 'mp3';
110
+ }
111
+ return 'unknown';
112
+ };
113
+ const getContainerExtension = container => {
114
+ switch (container) {
115
+ case 'webm':
116
+ return '.webm';
117
+ case 'ogg':
118
+ return '.ogg';
119
+ case 'wav':
120
+ return '.wav';
121
+ case 'mp4':
122
+ return '.m4a';
123
+ case 'mp3':
124
+ return '.mp3';
125
+ case 'aac':
126
+ return '.aac';
127
+ default:
128
+ return '.bin';
129
+ }
130
+ };
131
+
132
+ // AVFoundation (expo-av) and the iOS WKWebView <audio> element cannot
133
+ // demux/decode WebM or Ogg Opus — only `AudioContext.decodeAudioData` can
134
+ // on iOS. So on iOS those two containers go through the WebView decoder.
135
+ // Everything else (mp3/m4a/aac/wav), and ALL of Android (ExoPlayer handles
136
+ // Opus natively), plays straight through expo-av.
137
+ const requiresWebAudioDecode = container => _reactNative.Platform.OS === 'ios' && (container === 'webm' || container === 'ogg');
138
+
139
+ // The WebView is used ONLY as a decoder, never as a player. Two iOS facts
140
+ // force this shape:
141
+ // • `decodeAudioData` is the only iOS API that understands WebM/Ogg Opus,
142
+ // and it works on a suspended AudioContext (decoding needs no gesture).
143
+ // • Playing audio OUT of a WebView — via AudioContext OR an <audio>
144
+ // element — is gated behind an in-page user gesture that an RN-side tap
145
+ // cannot supply, so both hang silently.
146
+ // Therefore the page decodes Opus → PCM, re-encodes to a WAV blob, and ships
147
+ // the bytes back to RN as base64. RN writes the WAV to cache and plays it
148
+ // through expo-av (native, no gesture limits, real play/pause/seek).
149
+ const WEB_DECODER_HTML = `<!doctype html>
150
+ <html>
151
+ <head>
152
+ <meta charset="utf-8" />
153
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
154
+ <style>html,body{margin:0;padding:0;width:1px;height:1px;overflow:hidden;background:transparent}</style>
155
+ </head>
156
+ <body>
157
+ <script>
158
+ (function () {
159
+ var MAX_WAV = ${MAX_DECODE_BYTES};
160
+ var post = function (payload) {
161
+ if (window.ReactNativeWebView) {
162
+ window.ReactNativeWebView.postMessage(JSON.stringify(payload));
163
+ }
164
+ };
165
+ var ctx = null;
166
+ var encodeWav = function (buffer) {
167
+ var numCh = buffer.numberOfChannels;
168
+ var frames = buffer.length;
169
+ var sr = buffer.sampleRate;
170
+ var bytesLen = 44 + frames * numCh * 2;
171
+ var ab = new ArrayBuffer(bytesLen);
172
+ var view = new DataView(ab);
173
+ var off = 0;
174
+ var ws = function (s) { for (var i = 0; i < s.length; i += 1) { view.setUint8(off++, s.charCodeAt(i)); } };
175
+ var u32 = function (d) { view.setUint32(off, d, true); off += 4; };
176
+ var u16 = function (d) { view.setUint16(off, d, true); off += 2; };
177
+ ws('RIFF'); u32(bytesLen - 8); ws('WAVE');
178
+ ws('fmt '); u32(16); u16(1); u16(numCh); u32(sr); u32(sr * numCh * 2); u16(numCh * 2); u16(16);
179
+ ws('data'); u32(frames * numCh * 2);
180
+ var chans = [];
181
+ for (var c = 0; c < numCh; c += 1) { chans.push(buffer.getChannelData(c)); }
182
+ for (var f = 0; f < frames; f += 1) {
183
+ for (var c2 = 0; c2 < numCh; c2 += 1) {
184
+ var s = Math.max(-1, Math.min(1, chans[c2][f]));
185
+ view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7fff, true);
186
+ off += 2;
187
+ }
188
+ }
189
+ return ab;
190
+ };
191
+ var abToBase64 = function (ab) {
192
+ var bytes = new Uint8Array(ab);
193
+ var binary = '';
194
+ var chunk = 0x8000;
195
+ for (var i = 0; i < bytes.length; i += chunk) {
196
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
197
+ }
198
+ return window.btoa(binary);
199
+ };
200
+ window.__decodeAudio = function (base64) {
201
+ try {
202
+ if (!ctx) {
203
+ var Ctx = window.AudioContext || window.webkitAudioContext;
204
+ ctx = new Ctx();
205
+ }
206
+ var bin = window.atob(base64);
207
+ var len = bin.length;
208
+ var bytes = new Uint8Array(len);
209
+ for (var i = 0; i < len; i += 1) { bytes[i] = bin.charCodeAt(i); }
210
+ ctx.decodeAudioData(
211
+ bytes.buffer,
212
+ function (decoded) {
213
+ try {
214
+ var wav = encodeWav(decoded);
215
+ if (wav.byteLength > MAX_WAV) {
216
+ post({ type: 'error', message: 'audio_too_long' });
217
+ return;
218
+ }
219
+ post({
220
+ type: 'decoded',
221
+ durationMillis: Math.floor(decoded.duration * 1000),
222
+ wavBase64: abToBase64(wav),
223
+ });
224
+ } catch (e) {
225
+ post({ type: 'error', message: 'encode_failed:' + (e && e.message ? e.message : e) });
226
+ }
227
+ },
228
+ function (err) {
229
+ post({ type: 'error', message: 'decode_failed:' + (err && err.message ? err.message : err) });
230
+ }
231
+ );
232
+ } catch (e) {
233
+ post({ type: 'error', message: 'load_failed:' + (e && e.message ? e.message : e) });
234
+ }
235
+ };
236
+ post({ type: 'bridge_ready' });
237
+ })();
238
+ </script>
239
+ </body>
240
+ </html>`;
18
241
  const AudioMessage = ({
19
- src
242
+ src,
243
+ mimeType,
244
+ fileName,
245
+ originalName
20
246
  }) => {
247
+ const {
248
+ config
249
+ } = (0, _useChatSettingState.useChatSettingState)();
21
250
  const soundRef = (0, _react.useRef)(null);
251
+ const webViewRef = (0, _react.useRef)(null);
252
+ const preparedRef = (0, _react.useRef)(null);
253
+ const bridgeReadyRef = (0, _react.useRef)(false);
254
+ const injectedRef = (0, _react.useRef)(false);
255
+ const didFinishRef = (0, _react.useRef)(false);
256
+ const unmountedRef = (0, _react.useRef)(false);
257
+ const loadingTimerRef = (0, _react.useRef)(null);
22
258
  const [isPlaying, setIsPlaying] = (0, _react.useState)(false);
23
259
  const [isLoading, setIsLoading] = (0, _react.useState)(false);
260
+ const [playbackError, setPlaybackError] = (0, _react.useState)(false);
24
261
  const [position, setPosition] = (0, _react.useState)(0);
25
262
  const [duration, setDuration] = (0, _react.useState)(0);
263
+ const [webViewEnabled, setWebViewEnabled] = (0, _react.useState)(false);
264
+ const primaryColor = config?.colors?.primary || '#0A84FF';
265
+ const clearLoadingGuard = () => {
266
+ if (loadingTimerRef.current) {
267
+ clearTimeout(loadingTimerRef.current);
268
+ loadingTimerRef.current = null;
269
+ }
270
+ };
271
+ const fail = reason => {
272
+ clearLoadingGuard();
273
+ if (unmountedRef.current) {
274
+ return;
275
+ }
276
+ logAudioDebug('audio playback failed', {
277
+ src,
278
+ mimeType,
279
+ fileName,
280
+ originalName,
281
+ container: preparedRef.current?.container,
282
+ error: reason instanceof Error ? {
283
+ name: reason.name,
284
+ message: reason.message
285
+ } : String(reason)
286
+ });
287
+ setIsLoading(false);
288
+ setIsPlaying(false);
289
+ setPlaybackError(true);
290
+ };
291
+
292
+ // Hard ceiling on the loading spinner — whatever stalls (a hung network
293
+ // request, a WebView that never decodes, expo-av wedging on a bad file)
294
+ // the control resolves to an error state instead of spinning forever
295
+ // (the customer-reported "stuck spinner").
296
+ const armLoadingGuard = () => {
297
+ clearLoadingGuard();
298
+ loadingTimerRef.current = setTimeout(() => {
299
+ if (!unmountedRef.current && !soundRef.current) {
300
+ fail(new Error('audio_timeout'));
301
+ }
302
+ }, 15000);
303
+ };
304
+ const prepareSource = async () => {
305
+ if (preparedRef.current) {
306
+ return preparedRef.current;
307
+ }
308
+ if (!/^https?:\/\//i.test(src)) {
309
+ const prepared = {
310
+ localUri: src,
311
+ container: 'unknown'
312
+ };
313
+ preparedRef.current = prepared;
314
+ return prepared;
315
+ }
316
+ const cacheDirectory = FileSystem.cacheDirectory;
317
+ if (!cacheDirectory) {
318
+ const prepared = {
319
+ localUri: src,
320
+ container: 'unknown'
321
+ };
322
+ preparedRef.current = prepared;
323
+ return prepared;
324
+ }
325
+ const base = `${cacheDirectory}ethora-audio-${hashString(src)}`;
326
+ const part = `${base}.part`;
327
+ const partInfo = await FileSystem.getInfoAsync(part);
328
+ if (!partInfo.exists) {
329
+ const download = await withTimeout(FileSystem.downloadAsync(src, part), 12000, 'audio_download_timeout');
330
+ logAudioDebug('audio downloaded', {
331
+ src,
332
+ uri: download.status === 200 ? download.uri : undefined,
333
+ status: download.status
334
+ });
335
+ if (download.status !== 200) {
336
+ throw new Error(`audio_download_status_${download.status}`);
337
+ }
338
+ }
339
+ const head = await FileSystem.readAsStringAsync(part, {
340
+ encoding: FileSystem.EncodingType.Base64,
341
+ position: 0,
342
+ length: 32
343
+ });
344
+ const container = detectContainer(decodeBase64Head(head, 16));
345
+ let base64;
346
+ let localUri = part;
347
+ if (requiresWebAudioDecode(container)) {
348
+ // WebView-decode path: extension is irrelevant, we hand over the bytes.
349
+ const info = await FileSystem.getInfoAsync(part);
350
+ const size = 'size' in info ? info.size ?? 0 : 0;
351
+ if (size > MAX_DECODE_BYTES) {
352
+ throw new Error('audio_too_large_to_decode');
353
+ }
354
+ base64 = await FileSystem.readAsStringAsync(part, {
355
+ encoding: FileSystem.EncodingType.Base64
356
+ });
357
+ } else {
358
+ // Native path: give the cached file a real extension so AVFoundation
359
+ // / ExoPlayer pick the right demuxer instead of choking on an opaque
360
+ // `.blob` (the original `audio_timeout` cause).
361
+ const ext = container !== 'unknown' ? getContainerExtension(container) : (0, _mimeToExtension.getIosAudioPlaybackCacheExtension)({
362
+ mime: mimeType,
363
+ fileName,
364
+ originalName,
365
+ url: src
366
+ });
367
+ const finalUri = `${base}${ext}`;
368
+ const finalInfo = await FileSystem.getInfoAsync(finalUri);
369
+ if (finalInfo.exists) {
370
+ await FileSystem.deleteAsync(part, {
371
+ idempotent: true
372
+ }).catch(() => {});
373
+ } else {
374
+ await FileSystem.moveAsync({
375
+ from: part,
376
+ to: finalUri
377
+ });
378
+ }
379
+ localUri = finalUri;
380
+ }
381
+ const prepared = {
382
+ localUri,
383
+ container,
384
+ base64
385
+ };
386
+ preparedRef.current = prepared;
387
+ logAudioDebug('audio prepared', {
388
+ src,
389
+ mimeType,
390
+ fileName,
391
+ originalName,
392
+ container,
393
+ platform: _reactNative.Platform.OS,
394
+ decode: requiresWebAudioDecode(container) ? 'web-audio' : 'native'
395
+ });
396
+ return prepared;
397
+ };
398
+ const maybeInjectDecode = () => {
399
+ const base64 = preparedRef.current?.base64;
400
+ if (bridgeReadyRef.current && base64 && !injectedRef.current && webViewRef.current) {
401
+ injectedRef.current = true;
402
+ webViewRef.current.injectJavaScript(`window.__decodeAudio(${JSON.stringify(base64)}); true;`);
403
+ }
404
+ };
26
405
  const onPlaybackStatusUpdate = status => {
27
- if (!status.isLoaded) return;
406
+ if (!status.isLoaded) {
407
+ if (status.error) {
408
+ logAudioDebug('playback status error', {
409
+ src,
410
+ error: status.error
411
+ });
412
+ }
413
+ return;
414
+ }
28
415
  setPosition(status.positionMillis);
29
416
  setDuration(status.durationMillis ?? 0);
30
417
  setIsPlaying(status.isPlaying);
31
418
  if (status.didJustFinish) {
419
+ didFinishRef.current = true;
32
420
  setIsPlaying(false);
33
- soundRef.current?.setPositionAsync(0);
421
+ setPosition(0);
422
+ void soundRef.current?.setStatusAsync({
423
+ shouldPlay: false,
424
+ positionMillis: 0
425
+ });
426
+ }
427
+ };
428
+ const startNativePlayback = async localUri => {
429
+ try {
430
+ await _expoAv.Audio.setAudioModeAsync({
431
+ playsInSilentModeIOS: true
432
+ });
433
+ } catch {
434
+ /* non-fatal — still plays through the ringer channel */
435
+ }
436
+ const {
437
+ sound
438
+ } = await withTimeout(_expoAv.Audio.Sound.createAsync({
439
+ uri: localUri
440
+ }, {
441
+ shouldPlay: true
442
+ }, onPlaybackStatusUpdate), 12000, 'audio_create_timeout');
443
+ if (unmountedRef.current) {
444
+ void sound.unloadAsync();
445
+ return;
446
+ }
447
+ soundRef.current = sound;
448
+ clearLoadingGuard();
449
+ setIsLoading(false);
450
+ setPlaybackError(false);
451
+ setIsPlaying(true);
452
+ logAudioDebug('native audio playback started', {
453
+ src,
454
+ container: preparedRef.current?.container,
455
+ localUri
456
+ });
457
+ };
458
+
459
+ // WebView finished decoding Opus → WAV: persist the WAV and play it
460
+ // natively. From here it is indistinguishable from any other native clip.
461
+ const onDecoded = async (durationMillis, wavBase64) => {
462
+ try {
463
+ const cacheDirectory = FileSystem.cacheDirectory || '';
464
+ const wavUri = `${cacheDirectory}ethora-audio-${hashString(src)}.wav`;
465
+ await FileSystem.writeAsStringAsync(wavUri, wavBase64, {
466
+ encoding: FileSystem.EncodingType.Base64
467
+ });
468
+ if (unmountedRef.current) {
469
+ return;
470
+ }
471
+ preparedRef.current = {
472
+ localUri: wavUri,
473
+ container: preparedRef.current?.container ?? 'webm'
474
+ };
475
+ if (durationMillis > 0) {
476
+ setDuration(durationMillis);
477
+ }
478
+ setWebViewEnabled(false); // decoder no longer needed
479
+ await startNativePlayback(wavUri);
480
+ } catch (error) {
481
+ fail(error);
34
482
  }
35
483
  };
484
+ const handleWebMessage = event => {
485
+ let payload;
486
+ try {
487
+ payload = JSON.parse(event.nativeEvent.data);
488
+ } catch {
489
+ return;
490
+ }
491
+ if (payload.type === 'bridge_ready') {
492
+ bridgeReadyRef.current = true;
493
+ maybeInjectDecode();
494
+ return;
495
+ }
496
+ if (payload.type === 'decoded') {
497
+ logAudioDebug('web audio decoded', {
498
+ src,
499
+ durationMillis: payload.durationMillis,
500
+ wavBytes: payload.wavBase64 ? payload.wavBase64.length : 0
501
+ });
502
+ void onDecoded(payload.durationMillis, payload.wavBase64);
503
+ return;
504
+ }
505
+ // type === 'error'
506
+ fail(new Error(payload.message || 'web_audio_error'));
507
+ };
36
508
  const togglePlayback = async () => {
509
+ if (!src) {
510
+ return;
511
+ }
512
+
513
+ // Already-loaded native sound (covers both native formats AND the
514
+ // decoded-WAV from the WebView path) → plain toggle.
515
+ if (soundRef.current) {
516
+ try {
517
+ if (isPlaying) {
518
+ await soundRef.current.pauseAsync();
519
+ } else {
520
+ if (didFinishRef.current) {
521
+ await soundRef.current.setPositionAsync(0);
522
+ didFinishRef.current = false;
523
+ }
524
+ await soundRef.current.playAsync();
525
+ }
526
+ } catch (error) {
527
+ fail(error);
528
+ }
529
+ return;
530
+ }
531
+
532
+ // First tap → prepare (download + sniff) then route to the right engine.
533
+ setPlaybackError(false);
534
+ setIsLoading(true);
535
+ armLoadingGuard();
37
536
  try {
38
- if (!soundRef.current) {
39
- setIsLoading(true);
40
- const {
41
- sound
42
- } = await _expoAv.Audio.Sound.createAsync({
43
- uri: src
44
- }, {
45
- shouldPlay: true
46
- }, onPlaybackStatusUpdate);
47
- soundRef.current = sound;
48
- setIsLoading(false);
49
- setIsPlaying(true);
537
+ const prepared = await withTimeout(prepareSource(), 14000, 'audio_prepare_timeout');
538
+ if (unmountedRef.current) {
50
539
  return;
51
540
  }
52
- if (isPlaying) {
53
- await soundRef.current.pauseAsync();
54
- } else {
55
- await soundRef.current.playAsync();
541
+ if (requiresWebAudioDecode(prepared.container)) {
542
+ // Mount the decoder WebView; decoding + playback continue when the
543
+ // 'decoded' message arrives (→ onDecoded → startNativePlayback).
544
+ if (!webViewEnabled) {
545
+ setWebViewEnabled(true);
546
+ }
547
+ maybeInjectDecode();
548
+ return;
56
549
  }
550
+ await startNativePlayback(prepared.localUri);
57
551
  } catch (error) {
58
- console.error('Audio playback error:', error);
59
- setIsLoading(false);
552
+ fail(error);
60
553
  }
61
554
  };
62
555
  (0, _react.useEffect)(() => {
556
+ unmountedRef.current = false;
63
557
  return () => {
558
+ unmountedRef.current = true;
559
+ clearLoadingGuard();
64
560
  soundRef.current?.unloadAsync();
65
561
  };
562
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
563
  }, []);
67
- const progress = duration > 0 ? position / duration : 0;
564
+
565
+ // Full reset whenever the source changes (component reused for a new
566
+ // message / re-render with a different src).
567
+ (0, _react.useEffect)(() => {
568
+ soundRef.current?.unloadAsync();
569
+ soundRef.current = null;
570
+ preparedRef.current = null;
571
+ bridgeReadyRef.current = false;
572
+ injectedRef.current = false;
573
+ didFinishRef.current = false;
574
+ clearLoadingGuard();
575
+ setIsPlaying(false);
576
+ setIsLoading(false);
577
+ setPlaybackError(false);
578
+ setPosition(0);
579
+ setDuration(0);
580
+ setWebViewEnabled(false);
581
+ // eslint-disable-next-line react-hooks/exhaustive-deps
582
+ }, [src]);
583
+ const progress = duration > 0 ? Math.min(1, position / duration) : 0;
68
584
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
69
- style: styles.container,
70
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
71
- style: styles.playButton,
72
- onPress: togglePlayback,
73
- disabled: isLoading,
74
- children: isLoading ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
75
- size: "small",
76
- color: "#fff"
77
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
78
- style: styles.playIcon,
79
- children: isPlaying ? '⏸' : '▶'
585
+ style: styles.root,
586
+ children: [webViewEnabled ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
587
+ style: styles.decoderHost,
588
+ pointerEvents: "none",
589
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeWebview.WebView, {
590
+ ref: webViewRef,
591
+ source: {
592
+ html: WEB_DECODER_HTML
593
+ },
594
+ originWhitelist: ['*'],
595
+ onMessage: handleWebMessage,
596
+ javaScriptEnabled: true,
597
+ domStorageEnabled: false,
598
+ scrollEnabled: false,
599
+ pointerEvents: "none",
600
+ showsHorizontalScrollIndicator: false,
601
+ showsVerticalScrollIndicator: false,
602
+ style: styles.hiddenWebView
80
603
  })
81
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
82
- style: styles.progressContainer,
83
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
84
- style: styles.progressTrack,
85
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
86
- style: [styles.progressFill, {
87
- width: `${progress * 100}%`
88
- }]
604
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
605
+ style: styles.container,
606
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
607
+ style: [styles.playButton, {
608
+ backgroundColor: primaryColor
609
+ }],
610
+ onPress: togglePlayback,
611
+ disabled: isLoading,
612
+ activeOpacity: 0.85,
613
+ children: isLoading ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ActivityIndicator, {
614
+ size: "small",
615
+ color: "#fff"
616
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
617
+ style: styles.iconWrap,
618
+ children: isPlaying ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.PauseIcon, {
619
+ width: 18,
620
+ height: 18,
621
+ color: "#fff"
622
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_icons.PlayIcon, {
623
+ width: 18,
624
+ height: 18,
625
+ color: "#fff"
626
+ })
89
627
  })
90
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
91
- style: styles.time,
92
- children: [formatTime(position), " / ", formatTime(duration)]
628
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
629
+ style: styles.progressContainer,
630
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
631
+ style: styles.progressTrack,
632
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
633
+ style: [styles.progressFill, {
634
+ width: `${progress * 100}%`,
635
+ backgroundColor: primaryColor
636
+ }]
637
+ })
638
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
639
+ style: styles.progressRow,
640
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
641
+ style: styles.time,
642
+ children: playbackError ? 'Audio unavailable' : formatTime(position)
643
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
644
+ style: styles.time,
645
+ children: !playbackError && duration > 0 ? formatTime(duration) : ''
646
+ })]
647
+ })]
93
648
  })]
94
649
  })]
95
650
  });
96
651
  };
97
652
  var _default = exports.default = AudioMessage;
98
653
  const styles = _reactNative.StyleSheet.create({
654
+ root: {
655
+ position: 'relative'
656
+ },
657
+ decoderHost: {
658
+ position: 'absolute',
659
+ top: 0,
660
+ left: 0,
661
+ width: 0,
662
+ height: 0,
663
+ overflow: 'hidden',
664
+ opacity: 0
665
+ },
99
666
  container: {
100
667
  flexDirection: 'row',
101
668
  alignItems: 'center',
102
- paddingHorizontal: 8,
103
- paddingVertical: 6,
104
- minWidth: 200
669
+ gap: 12,
670
+ paddingHorizontal: 12,
671
+ paddingVertical: 10,
672
+ width: 260,
673
+ maxWidth: '100%'
105
674
  },
106
675
  playButton: {
107
- width: 36,
108
- height: 36,
109
- borderRadius: 18,
110
- backgroundColor: '#007AFF',
676
+ width: 44,
677
+ height: 44,
678
+ borderRadius: 22,
111
679
  alignItems: 'center',
112
- justifyContent: 'center',
113
- marginRight: 10
680
+ justifyContent: 'center'
114
681
  },
115
- playIcon: {
116
- color: '#fff',
117
- fontSize: 14
682
+ iconWrap: {
683
+ marginLeft: 1
118
684
  },
119
685
  progressContainer: {
120
- flex: 1
686
+ flex: 1,
687
+ gap: 6
688
+ },
689
+ progressRow: {
690
+ flexDirection: 'row',
691
+ justifyContent: 'space-between',
692
+ alignItems: 'center'
121
693
  },
122
694
  progressTrack: {
123
- height: 4,
124
- backgroundColor: '#E0E0E0',
125
- borderRadius: 2,
695
+ height: 6,
696
+ backgroundColor: '#D0D7E6',
697
+ borderRadius: 999,
126
698
  overflow: 'hidden'
127
699
  },
700
+ hiddenWebView: {
701
+ position: 'absolute',
702
+ width: 1,
703
+ height: 1,
704
+ opacity: 0
705
+ },
128
706
  progressFill: {
129
707
  height: '100%',
130
- backgroundColor: '#007AFF',
131
- borderRadius: 2
708
+ borderRadius: 999
132
709
  },
133
710
  time: {
134
- fontSize: 11,
135
- color: '#888',
136
- marginTop: 4
711
+ fontSize: 12,
712
+ color: '#667085',
713
+ fontWeight: '500'
137
714
  }
138
715
  });
139
716
  //# sourceMappingURL=AudioMessage.js.map