@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.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -0
- package/lib/commonjs/assets/icons.js +7 -1
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +58 -22
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +288 -5
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js +23 -3
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js +5 -44
- package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +149 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +4 -17
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +19 -33
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/AudioMessage.js +643 -66
- package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/Button.js +9 -3
- package/lib/commonjs/components/styled/Button.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +239 -40
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js +31 -18
- package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
- package/lib/commonjs/components/styled/UnsupportedType.js +47 -5
- package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +68 -2
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/keyboardLayout.js +5 -3
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/commonjs/helpers/mimeToExtension.js +70 -3
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/hooks/useEventHandlers.js +1 -1
- package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +6 -3
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +26 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +65 -4
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/assets/icons.js +7 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +59 -23
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +290 -7
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js +24 -4
- package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js +5 -45
- package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +144 -0
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js +3 -17
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +20 -34
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/module/components/styled/AudioMessage.js +644 -67
- package/lib/module/components/styled/AudioMessage.js.map +1 -1
- package/lib/module/components/styled/Button.js +9 -3
- package/lib/module/components/styled/Button.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +240 -42
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/StyledInputComponents/MediaComponents.js +31 -18
- package/lib/module/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
- package/lib/module/components/styled/UnsupportedType.js +47 -5
- package/lib/module/components/styled/UnsupportedType.js.map +1 -1
- package/lib/module/context/xmppProvider.js +70 -4
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/keyboardLayout.js +4 -2
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/helpers/mimeToExtension.js +67 -3
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/hooks/useEventHandlers.js +1 -1
- package/lib/module/hooks/useEventHandlers.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +6 -3
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/networking/xmppClient.js +26 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/module/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +63 -3
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts +0 -7
- package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts +8 -0
- package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/AudioMessage.d.ts +8 -2
- package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/Button.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts +5 -1
- package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/UnsupportedType.d.ts +5 -0
- package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts +3 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts +31 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/Middleware/unreadMidlleware.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +18 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/modal.model.d.ts +3 -0
- package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +15 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/assets/icons.tsx +30 -25
- package/src/components/MainComponents/ChatRoom.tsx +61 -23
- package/src/components/MainComponents/MediaMessage.tsx +295 -5
- package/src/components/MainComponents/ReduxWrapper.tsx +23 -9
- package/src/components/MessageBubble/MessageInteractions.tsx +35 -3
- package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -3
- package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
- package/src/components/RoomComponents/styled/LastAudioMessage.tsx +20 -35
- package/src/components/styled/AudioMessage.tsx +724 -59
- package/src/components/styled/Button.tsx +20 -11
- package/src/components/styled/SendInput.tsx +255 -46
- package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
- package/src/components/styled/UnsupportedType.tsx +60 -6
- package/src/context/xmppProvider.tsx +61 -3
- package/src/helpers/keyboardLayout.ts +5 -1
- package/src/helpers/mimeToExtension.ts +116 -2
- package/src/hooks/useEventHandlers.tsx +1 -1
- package/src/hooks/useSendMessage.tsx +12 -6
- package/src/networking/xmppClient.ts +24 -1
- package/src/roomStore/Middleware/unreadMidlleware.tsx +11 -8
- package/src/roomStore/roomsSlice.ts +66 -2
- package/src/types/models/modal.model.ts +3 -0
- 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
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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 (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
70
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.
|
|
71
|
-
style: styles.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
83
|
-
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.
|
|
84
|
-
style: styles.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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.
|
|
91
|
-
style: styles.
|
|
92
|
-
children: [
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
669
|
+
gap: 12,
|
|
670
|
+
paddingHorizontal: 12,
|
|
671
|
+
paddingVertical: 10,
|
|
672
|
+
width: 260,
|
|
673
|
+
maxWidth: '100%'
|
|
105
674
|
},
|
|
106
675
|
playButton: {
|
|
107
|
-
width:
|
|
108
|
-
height:
|
|
109
|
-
borderRadius:
|
|
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
|
-
|
|
116
|
-
|
|
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:
|
|
124
|
-
backgroundColor: '#
|
|
125
|
-
borderRadius:
|
|
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
|
-
|
|
131
|
-
borderRadius: 2
|
|
708
|
+
borderRadius: 999
|
|
132
709
|
},
|
|
133
710
|
time: {
|
|
134
|
-
fontSize:
|
|
135
|
-
color: '#
|
|
136
|
-
|
|
711
|
+
fontSize: 12,
|
|
712
|
+
color: '#667085',
|
|
713
|
+
fontWeight: '500'
|
|
137
714
|
}
|
|
138
715
|
});
|
|
139
716
|
//# sourceMappingURL=AudioMessage.js.map
|