@repzo/widget-react-native 1.2.1 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +40 -3
  3. package/android/build.gradle +22 -0
  4. package/android/src/main/java/expo/modules/repzowidget/RepzoVoicePlayer.kt +222 -0
  5. package/android/src/main/java/expo/modules/repzowidget/RepzoWidgetAudioModule.kt +68 -0
  6. package/expo-module.config.json +9 -0
  7. package/ios/RepzoVoicePlayer.swift +175 -0
  8. package/ios/RepzoWidgetAudio.podspec +26 -0
  9. package/ios/RepzoWidgetAudioModule.swift +65 -0
  10. package/lib/commonjs/api/client.js +47 -2
  11. package/lib/commonjs/api/client.js.map +1 -1
  12. package/lib/commonjs/audio/native-player.js +97 -0
  13. package/lib/commonjs/audio/native-player.js.map +1 -0
  14. package/lib/commonjs/audio/voice.js +146 -0
  15. package/lib/commonjs/audio/voice.js.map +1 -0
  16. package/lib/commonjs/i18n.js +6 -0
  17. package/lib/commonjs/i18n.js.map +1 -1
  18. package/lib/commonjs/locales.js +4 -0
  19. package/lib/commonjs/locales.js.map +1 -1
  20. package/lib/commonjs/state/use-widget-state.js +31 -0
  21. package/lib/commonjs/state/use-widget-state.js.map +1 -1
  22. package/lib/commonjs/state/widget-store.js +44 -1
  23. package/lib/commonjs/state/widget-store.js.map +1 -1
  24. package/lib/commonjs/ui/WidgetProvider.js +7 -3
  25. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  26. package/lib/commonjs/ui/components/FileCard.js +134 -0
  27. package/lib/commonjs/ui/components/FileCard.js.map +1 -0
  28. package/lib/commonjs/ui/components/ImageViewer.js +17 -35
  29. package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
  30. package/lib/commonjs/ui/components/Launcher.js +15 -1
  31. package/lib/commonjs/ui/components/Launcher.js.map +1 -1
  32. package/lib/commonjs/ui/components/MessagesSkeleton.js +126 -0
  33. package/lib/commonjs/ui/components/MessagesSkeleton.js.map +1 -0
  34. package/lib/commonjs/ui/components/VoiceNote.js +353 -0
  35. package/lib/commonjs/ui/components/VoiceNote.js.map +1 -0
  36. package/lib/commonjs/ui/components/ZoomableImage.js +299 -0
  37. package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -0
  38. package/lib/commonjs/ui/components/icons.js +57 -0
  39. package/lib/commonjs/ui/components/icons.js.map +1 -1
  40. package/lib/commonjs/ui/screens/chat/ChatView.js +39 -18
  41. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  42. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +5 -16
  43. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
  44. package/lib/module/api/client.js +48 -2
  45. package/lib/module/api/client.js.map +1 -1
  46. package/lib/module/audio/native-player.js +93 -0
  47. package/lib/module/audio/native-player.js.map +1 -0
  48. package/lib/module/audio/voice.js +136 -0
  49. package/lib/module/audio/voice.js.map +1 -0
  50. package/lib/module/i18n.js +6 -0
  51. package/lib/module/i18n.js.map +1 -1
  52. package/lib/module/locales.js +4 -0
  53. package/lib/module/locales.js.map +1 -1
  54. package/lib/module/state/use-widget-state.js +30 -1
  55. package/lib/module/state/use-widget-state.js.map +1 -1
  56. package/lib/module/state/widget-store.js +44 -1
  57. package/lib/module/state/widget-store.js.map +1 -1
  58. package/lib/module/ui/WidgetProvider.js +7 -3
  59. package/lib/module/ui/WidgetProvider.js.map +1 -1
  60. package/lib/module/ui/components/FileCard.js +130 -0
  61. package/lib/module/ui/components/FileCard.js.map +1 -0
  62. package/lib/module/ui/components/ImageViewer.js +18 -36
  63. package/lib/module/ui/components/ImageViewer.js.map +1 -1
  64. package/lib/module/ui/components/Launcher.js +16 -2
  65. package/lib/module/ui/components/Launcher.js.map +1 -1
  66. package/lib/module/ui/components/MessagesSkeleton.js +124 -0
  67. package/lib/module/ui/components/MessagesSkeleton.js.map +1 -0
  68. package/lib/module/ui/components/VoiceNote.js +349 -0
  69. package/lib/module/ui/components/VoiceNote.js.map +1 -0
  70. package/lib/module/ui/components/ZoomableImage.js +291 -0
  71. package/lib/module/ui/components/ZoomableImage.js.map +1 -0
  72. package/lib/module/ui/components/icons.js +55 -0
  73. package/lib/module/ui/components/icons.js.map +1 -1
  74. package/lib/module/ui/screens/chat/ChatView.js +39 -18
  75. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  76. package/lib/module/ui/screens/chat/ChatView.styles.js +5 -16
  77. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
  78. package/lib/typescript/commonjs/api/client.d.ts +12 -0
  79. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/audio/native-player.d.ts +36 -0
  81. package/lib/typescript/commonjs/audio/native-player.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/audio/voice.d.ts +42 -0
  83. package/lib/typescript/commonjs/audio/voice.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/i18n.d.ts +4 -0
  85. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/state/use-widget-state.d.ts +8 -0
  88. package/lib/typescript/commonjs/state/use-widget-state.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/state/widget-store.d.ts +13 -0
  90. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/ui/components/FileCard.d.ts +21 -0
  93. package/lib/typescript/commonjs/ui/components/FileCard.d.ts.map +1 -0
  94. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +2 -2
  95. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/components/Launcher.d.ts.map +1 -1
  97. package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts +10 -0
  98. package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts.map +1 -0
  99. package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts +32 -0
  100. package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +45 -0
  102. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/ui/components/icons.d.ts +4 -0
  104. package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
  105. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +4 -16
  107. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +4 -0
  109. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  110. package/lib/typescript/module/api/client.d.ts +12 -0
  111. package/lib/typescript/module/api/client.d.ts.map +1 -1
  112. package/lib/typescript/module/audio/native-player.d.ts +36 -0
  113. package/lib/typescript/module/audio/native-player.d.ts.map +1 -0
  114. package/lib/typescript/module/audio/voice.d.ts +42 -0
  115. package/lib/typescript/module/audio/voice.d.ts.map +1 -0
  116. package/lib/typescript/module/i18n.d.ts +4 -0
  117. package/lib/typescript/module/i18n.d.ts.map +1 -1
  118. package/lib/typescript/module/locales.d.ts.map +1 -1
  119. package/lib/typescript/module/state/use-widget-state.d.ts +8 -0
  120. package/lib/typescript/module/state/use-widget-state.d.ts.map +1 -1
  121. package/lib/typescript/module/state/widget-store.d.ts +13 -0
  122. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/components/FileCard.d.ts +21 -0
  125. package/lib/typescript/module/ui/components/FileCard.d.ts.map +1 -0
  126. package/lib/typescript/module/ui/components/ImageViewer.d.ts +2 -2
  127. package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
  128. package/lib/typescript/module/ui/components/Launcher.d.ts.map +1 -1
  129. package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts +10 -0
  130. package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts.map +1 -0
  131. package/lib/typescript/module/ui/components/VoiceNote.d.ts +32 -0
  132. package/lib/typescript/module/ui/components/VoiceNote.d.ts.map +1 -0
  133. package/lib/typescript/module/ui/components/ZoomableImage.d.ts +45 -0
  134. package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -0
  135. package/lib/typescript/module/ui/components/icons.d.ts +4 -0
  136. package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
  137. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +4 -16
  139. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  140. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +4 -0
  141. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  142. package/package.json +14 -3
  143. package/react-native.config.js +12 -0
  144. package/src/api/client.ts +56 -2
  145. package/src/audio/native-player.ts +106 -0
  146. package/src/audio/voice.ts +127 -0
  147. package/src/i18n.ts +6 -0
  148. package/src/locales.ts +4 -0
  149. package/src/state/use-widget-state.ts +30 -1
  150. package/src/state/widget-store.ts +45 -1
  151. package/src/ui/WidgetProvider.tsx +7 -3
  152. package/src/ui/components/FileCard.tsx +111 -0
  153. package/src/ui/components/ImageViewer.tsx +11 -30
  154. package/src/ui/components/Launcher.tsx +7 -2
  155. package/src/ui/components/MessagesSkeleton.tsx +92 -0
  156. package/src/ui/components/VoiceNote.tsx +355 -0
  157. package/src/ui/components/ZoomableImage.tsx +250 -0
  158. package/src/ui/components/icons.tsx +33 -0
  159. package/src/ui/screens/chat/ChatView.styles.ts +5 -16
  160. package/src/ui/screens/chat/ChatView.tsx +43 -16
  161. package/src/ui/screens/chat/ChatView.types.ts +4 -0
@@ -0,0 +1,349 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * A voice note (or any audio attachment) played inside the chat, laid out the
5
+ * way messaging apps do it: a play button beside a waveform the visitor can
6
+ * tap or drag to jump through the note, the time under it, and a speed chip
7
+ * once the note has been started. Only rendered when the app can play audio
8
+ * (see `src/audio/voice.ts`); otherwise audio stays a file row.
9
+ */
10
+
11
+ import { useEffect, useMemo, useRef, useState } from 'react';
12
+ import { ActivityIndicator, PanResponder, Pressable, StyleSheet, Text, View } from 'react-native';
13
+ import { claimVoicePlayback, formatVoiceClock, useVoicePlayer, voiceWaveformLevels } from "../../audio/voice.js";
14
+ import { PauseIcon, PlayIcon } from "./icons.js";
15
+ import { fontSize, palette, withAlpha } from "../styles/theme.js";
16
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
17
+ const BAR_COUNT = 32;
18
+ const BAR_WIDTH = 3;
19
+ const BAR_MIN_HEIGHT = 4;
20
+ const BAR_MAX_HEIGHT = 24;
21
+ const BUTTON_SIZE = 36;
22
+ const KNOB_SIZE = 12;
23
+ const SPEEDS = [1, 1.5, 2];
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 function VoiceNote(props) {
31
+ const [started, setStarted] = useState(false);
32
+ if (started) return /*#__PURE__*/_jsx(ActiveVoiceNote, {
33
+ ...props
34
+ });
35
+ return /*#__PURE__*/_jsx(VoiceNoteLayout, {
36
+ ...props,
37
+ playing: false,
38
+ loading: false,
39
+ progress: 0,
40
+ time: null,
41
+ onToggle: () => setStarted(true)
42
+ });
43
+ }
44
+
45
+ /** A note that has been started: owns its player and plays on mount. */
46
+ function ActiveVoiceNote(props) {
47
+ const {
48
+ url,
49
+ onError
50
+ } = props;
51
+ const {
52
+ player,
53
+ status
54
+ } = useVoicePlayer(url);
55
+ const [speedIndex, setSpeedIndex] = useState(0);
56
+ const [scrub, setScrub] = useState(null);
57
+ const [trackWidth, setTrackWidth] = useState(0);
58
+ // Whether the visitor asked for playback, so a note still loading shows a
59
+ // spinner, and one they paused while it loaded doesn't.
60
+ const [wantsPlay, setWantsPlay] = useState(true);
61
+ // This note's hold on playback: starting it pauses any other note.
62
+ const releasePlayback = useRef(null);
63
+ const duration = Math.max(Number(status?.duration) || 0, 0);
64
+ const currentTime = Math.max(Number(status?.currentTime) || 0, 0);
65
+ const playing = !!status?.playing;
66
+ const finished = !!status?.didJustFinish || duration > 0 && currentTime >= duration - 0.1;
67
+ // `isLoaded` / `isBuffering` are compared strictly: a player that
68
+ // doesn't report them never gets a spinner that can't go away.
69
+ const loading = wantsPlay && !playing && (status?.isLoaded === false || status?.isBuffering === true);
70
+ const pause = () => {
71
+ setWantsPlay(false);
72
+ player.pause();
73
+ };
74
+ const start = () => {
75
+ releasePlayback.current?.();
76
+ releasePlayback.current = claimVoicePlayback(pause);
77
+ setWantsPlay(true);
78
+ player.play();
79
+ };
80
+
81
+ // The visitor tapped play on the idle note, which created this player.
82
+ // Neither callback changes for the player's lifetime.
83
+ useEffect(() => {
84
+ start();
85
+ return () => releasePlayback.current?.();
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ }, []);
88
+
89
+ // The error is only on the status update that reports it (the next one
90
+ // clears it), so it is acted on as it arrives.
91
+ const error = status?.error;
92
+ useEffect(() => {
93
+ if (error) onError();
94
+ }, [error, onError]);
95
+ const togglePlay = async () => {
96
+ if (playing || loading) {
97
+ pause();
98
+ return;
99
+ }
100
+ if (finished) await player.seekTo(0);
101
+ start();
102
+ };
103
+ const cycleSpeed = () => {
104
+ const next = (speedIndex + 1) % SPEEDS.length;
105
+ setSpeedIndex(next);
106
+ try {
107
+ player.setPlaybackRate(SPEEDS[next], 'high');
108
+ } catch {
109
+ // Some versions take no pitch-correction argument.
110
+ try {
111
+ player.setPlaybackRate(SPEEDS[next]);
112
+ } catch {
113
+ // Speed unsupported: the note keeps playing at 1×.
114
+ }
115
+ }
116
+ };
117
+
118
+ // The latest values for the (stable) PanResponder below.
119
+ const latest = useRef({
120
+ trackWidth,
121
+ duration,
122
+ player
123
+ });
124
+ latest.current = {
125
+ trackWidth,
126
+ duration,
127
+ player
128
+ };
129
+ const ratioAt = x => {
130
+ const width = latest.current.trackWidth;
131
+ return width > 0 ? Math.min(1, Math.max(0, x / width)) : 0;
132
+ };
133
+ // Where the finger first touched the track. Later positions are that plus
134
+ // the drag distance: a move event's own `locationX` is measured from
135
+ // whatever view is under the finger, not from the track.
136
+ const startX = useRef(0);
137
+ const pan = useRef(PanResponder.create({
138
+ onStartShouldSetPanResponder: () => true,
139
+ onMoveShouldSetPanResponder: () => true,
140
+ onPanResponderGrant: event => {
141
+ startX.current = event.nativeEvent.locationX;
142
+ setScrub(ratioAt(startX.current));
143
+ },
144
+ onPanResponderMove: (_, gesture) => setScrub(ratioAt(startX.current + gesture.dx)),
145
+ onPanResponderRelease: (_, gesture) => {
146
+ const {
147
+ duration: length,
148
+ player: current
149
+ } = latest.current;
150
+ if (length > 0) void current.seekTo(ratioAt(startX.current + gesture.dx) * length);
151
+ setScrub(null);
152
+ },
153
+ onPanResponderTerminate: () => setScrub(null)
154
+ })).current;
155
+ const displayTime = scrub != null ? scrub * duration : playing ? currentTime : finished ? duration : currentTime || duration;
156
+ // The chip joins once the note was started, so an untouched note shows only its length.
157
+ const showSpeed = speedIndex > 0 || playing || currentTime > 0 && !finished;
158
+ return /*#__PURE__*/_jsx(VoiceNoteLayout, {
159
+ ...props,
160
+ playing: playing,
161
+ loading: loading,
162
+ progress: scrub ?? (duration > 0 ? Math.min(1, currentTime / duration) : 0)
163
+ // Nothing to show until the player knows the note's length.
164
+ ,
165
+ time: duration > 0 ? formatVoiceClock(displayTime) : null,
166
+ onToggle: () => void togglePlay(),
167
+ track: {
168
+ panHandlers: pan.panHandlers,
169
+ onWidth: setTrackWidth
170
+ },
171
+ speed: showSpeed ? {
172
+ value: SPEEDS[speedIndex],
173
+ changed: speedIndex > 0,
174
+ onPress: cycleSpeed
175
+ } : null
176
+ });
177
+ }
178
+ function VoiceNoteLayout({
179
+ seed,
180
+ color,
181
+ onBrandBubble,
182
+ labels,
183
+ playing,
184
+ loading,
185
+ progress,
186
+ time,
187
+ onToggle,
188
+ track,
189
+ speed
190
+ }) {
191
+ const accent = onBrandBubble ? palette.white : color;
192
+ const onAccent = onBrandBubble ? color : palette.white;
193
+ const trackColor = onBrandBubble ? withAlpha(palette.white, 0.4) : palette.disabled;
194
+ const subtle = onBrandBubble ? withAlpha(palette.white, 0.8) : palette.textMuted;
195
+ const levels = useMemo(() => voiceWaveformLevels(seed, BAR_COUNT), [seed]);
196
+ const [trackWidth, setTrackWidth] = useState(0);
197
+ const onTrackLayout = event => {
198
+ setTrackWidth(event.nativeEvent.layout.width);
199
+ track?.onWidth(event.nativeEvent.layout.width);
200
+ };
201
+
202
+ // Bar i's centre sits at this share of the track; the playhead travels the same span.
203
+ const barCentre = i => trackWidth > BAR_WIDTH ? (BAR_WIDTH / 2 + i * (trackWidth - BAR_WIDTH) / (BAR_COUNT - 1)) / trackWidth : i / (BAR_COUNT - 1);
204
+ const knobOffset = trackWidth > 0 ? Math.round(progress * (trackWidth - KNOB_SIZE)) : 0;
205
+ let icon = /*#__PURE__*/_jsx(PlayIcon, {
206
+ size: 14,
207
+ color: onAccent
208
+ });
209
+ if (loading) icon = /*#__PURE__*/_jsx(ActivityIndicator, {
210
+ size: "small",
211
+ color: onAccent
212
+ });else if (playing) icon = /*#__PURE__*/_jsx(PauseIcon, {
213
+ size: 14,
214
+ color: onAccent
215
+ });
216
+ return /*#__PURE__*/_jsxs(View, {
217
+ style: styles.root,
218
+ children: [/*#__PURE__*/_jsxs(View, {
219
+ style: styles.row,
220
+ children: [/*#__PURE__*/_jsx(Pressable, {
221
+ onPress: onToggle,
222
+ accessibilityRole: "button",
223
+ accessibilityLabel: playing || loading ? labels.pause : labels.play,
224
+ accessibilityState: {
225
+ busy: loading
226
+ },
227
+ hitSlop: 4,
228
+ style: ({
229
+ pressed
230
+ }) => [styles.playButton, {
231
+ backgroundColor: accent,
232
+ opacity: pressed ? 0.8 : 1
233
+ }],
234
+ children: icon
235
+ }), /*#__PURE__*/_jsxs(View, {
236
+ style: styles.track,
237
+ onLayout: onTrackLayout,
238
+ ...track?.panHandlers,
239
+ children: [levels.map((level, i) => /*#__PURE__*/_jsx(View, {
240
+ // Touches land on the track itself, so their position is measured from its edge.
241
+ pointerEvents: "none",
242
+ style: [styles.bar, {
243
+ height: BAR_MIN_HEIGHT + level * (BAR_MAX_HEIGHT - BAR_MIN_HEIGHT),
244
+ backgroundColor: barCentre(i) <= progress ? accent : trackColor
245
+ }]
246
+ }, i)), trackWidth > 0 ? /*#__PURE__*/_jsx(View, {
247
+ pointerEvents: "none",
248
+ style: [styles.knob, {
249
+ backgroundColor: accent,
250
+ transform: [{
251
+ translateX: knobOffset
252
+ }]
253
+ }]
254
+ }) : null]
255
+ })]
256
+ }), /*#__PURE__*/_jsxs(View, {
257
+ style: styles.meta,
258
+ children: [time != null ? /*#__PURE__*/_jsx(Text, {
259
+ style: [styles.time, {
260
+ color: subtle
261
+ }],
262
+ children: time
263
+ }) : null, speed ? /*#__PURE__*/_jsx(Pressable, {
264
+ onPress: speed.onPress,
265
+ accessibilityRole: "button",
266
+ accessibilityLabel: labels.speed,
267
+ hitSlop: 12,
268
+ style: ({
269
+ pressed
270
+ }) => [styles.speed, {
271
+ backgroundColor: speed.changed ? accent : trackColor,
272
+ opacity: pressed ? 0.6 : 1
273
+ }],
274
+ children: /*#__PURE__*/_jsxs(Text, {
275
+ style: [styles.speedText, {
276
+ color: speed.changed ? onAccent : onBrandBubble ? palette.white : palette.text
277
+ }],
278
+ children: [speed.value, "\xD7"]
279
+ })
280
+ }) : null]
281
+ })]
282
+ });
283
+ }
284
+ const styles = StyleSheet.create({
285
+ // A timeline runs left to right in every language, so RTL doesn't mirror it
286
+ // (the touch position and the playhead stay in the same coordinates).
287
+ root: {
288
+ minWidth: 200,
289
+ direction: 'ltr'
290
+ },
291
+ row: {
292
+ flexDirection: 'row',
293
+ alignItems: 'center',
294
+ gap: 10
295
+ },
296
+ playButton: {
297
+ width: BUTTON_SIZE,
298
+ height: BUTTON_SIZE,
299
+ borderRadius: BUTTON_SIZE / 2,
300
+ alignItems: 'center',
301
+ justifyContent: 'center'
302
+ },
303
+ track: {
304
+ flex: 1,
305
+ minWidth: 0,
306
+ height: BUTTON_SIZE,
307
+ flexDirection: 'row',
308
+ alignItems: 'center',
309
+ justifyContent: 'space-between'
310
+ },
311
+ bar: {
312
+ width: BAR_WIDTH,
313
+ borderRadius: BAR_WIDTH / 2
314
+ },
315
+ knob: {
316
+ position: 'absolute',
317
+ left: 0,
318
+ width: KNOB_SIZE,
319
+ height: KNOB_SIZE,
320
+ borderRadius: KNOB_SIZE / 2
321
+ },
322
+ meta: {
323
+ minHeight: 18,
324
+ marginTop: -2,
325
+ marginLeft: BUTTON_SIZE + 10,
326
+ flexDirection: 'row',
327
+ alignItems: 'center',
328
+ gap: 6
329
+ },
330
+ time: {
331
+ fontSize: fontSize.caption,
332
+ fontWeight: '500',
333
+ fontVariant: ['tabular-nums']
334
+ },
335
+ speed: {
336
+ minWidth: 30,
337
+ height: 18,
338
+ paddingHorizontal: 6,
339
+ borderRadius: 9,
340
+ alignItems: 'center',
341
+ justifyContent: 'center'
342
+ },
343
+ speedText: {
344
+ fontSize: 11,
345
+ fontWeight: '700',
346
+ fontVariant: ['tabular-nums']
347
+ }
348
+ });
349
+ //# sourceMappingURL=VoiceNote.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useEffect","useMemo","useRef","useState","ActivityIndicator","PanResponder","Pressable","StyleSheet","Text","View","claimVoicePlayback","formatVoiceClock","useVoicePlayer","voiceWaveformLevels","PauseIcon","PlayIcon","fontSize","palette","withAlpha","jsx","_jsx","jsxs","_jsxs","BAR_COUNT","BAR_WIDTH","BAR_MIN_HEIGHT","BAR_MAX_HEIGHT","BUTTON_SIZE","KNOB_SIZE","SPEEDS","VoiceNote","props","started","setStarted","ActiveVoiceNote","VoiceNoteLayout","playing","loading","progress","time","onToggle","url","onError","player","status","speedIndex","setSpeedIndex","scrub","setScrub","trackWidth","setTrackWidth","wantsPlay","setWantsPlay","releasePlayback","duration","Math","max","Number","currentTime","finished","didJustFinish","isLoaded","isBuffering","pause","start","current","play","error","togglePlay","seekTo","cycleSpeed","next","length","setPlaybackRate","latest","ratioAt","x","width","min","startX","pan","create","onStartShouldSetPanResponder","onMoveShouldSetPanResponder","onPanResponderGrant","event","nativeEvent","locationX","onPanResponderMove","_","gesture","dx","onPanResponderRelease","onPanResponderTerminate","displayTime","showSpeed","track","panHandlers","onWidth","speed","value","changed","onPress","seed","color","onBrandBubble","labels","accent","white","onAccent","trackColor","disabled","subtle","textMuted","levels","onTrackLayout","layout","barCentre","i","knobOffset","round","icon","size","style","styles","root","children","row","accessibilityRole","accessibilityLabel","accessibilityState","busy","hitSlop","pressed","playButton","backgroundColor","opacity","onLayout","map","level","pointerEvents","bar","height","knob","transform","translateX","meta","speedText","text","minWidth","direction","flexDirection","alignItems","gap","borderRadius","justifyContent","flex","position","left","minHeight","marginTop","marginLeft","caption","fontWeight","fontVariant","paddingHorizontal"],"sourceRoot":"../../../../src","sources":["ui/components/VoiceNote.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,291 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * An image the visitor can pinch to zoom, drag around once zoomed, and
5
+ * double-tap to zoom in on a spot (and again to zoom back out). Built on
6
+ * PanResponder and Animated, so it behaves the same on iOS and Android
7
+ * without a gesture library in the host app.
8
+ */
9
+
10
+ import { useRef, useState } from 'react';
11
+ import { Animated, PanResponder, StyleSheet, View } from 'react-native';
12
+ import { useReduceMotion } from "../hooks/use-reduce-motion.js";
13
+ import { jsx as _jsx } from "react/jsx-runtime";
14
+ export const MAX_ZOOM = 4;
15
+ export const DOUBLE_TAP_ZOOM = 2.5;
16
+ const DOUBLE_TAP_MS = 300;
17
+ const DOUBLE_TAP_SLOP = 30;
18
+ const TAP_SLOP = 10;
19
+
20
+ /** Scale and offset (from the centre of the view) of the image. */
21
+
22
+ /** The size the image is drawn at with `contain`, before any zoom. */
23
+ export function fittedSize(box, image) {
24
+ if (!image || image.width <= 0 || image.height <= 0) return box;
25
+ const fit = Math.min(box.width / image.width, box.height / image.height);
26
+ return {
27
+ width: image.width * fit,
28
+ height: image.height * fit
29
+ };
30
+ }
31
+
32
+ /**
33
+ * Keeps the image inside the screen: zoom between 1× and `MAX_ZOOM`, and no
34
+ * empty space past an edge the zoomed image is wider (or taller) than.
35
+ */
36
+ export function clampView(view, box, content) {
37
+ const scale = Math.min(MAX_ZOOM, Math.max(1, view.scale));
38
+ const maxX = Math.max(0, (content.width * scale - box.width) / 2);
39
+ const maxY = Math.max(0, (content.height * scale - box.height) / 2);
40
+ return {
41
+ scale,
42
+ x: Math.min(maxX, Math.max(-maxX, view.x)),
43
+ y: Math.min(maxY, Math.max(-maxY, view.y))
44
+ };
45
+ }
46
+
47
+ /**
48
+ * The view at `scale` that keeps the spot that was under `from` (a point
49
+ * relative to the centre, in the `start` view) under `to`. Pinching zooms
50
+ * around the fingers this way, and moving them pans at the same time.
51
+ */
52
+ export function zoomAround(start, scale, from, to) {
53
+ const spotX = (from.x - start.x) / start.scale;
54
+ const spotY = (from.y - start.y) / start.scale;
55
+ return {
56
+ scale,
57
+ x: to.x - scale * spotX,
58
+ y: to.y - scale * spotY
59
+ };
60
+ }
61
+ const centreOf = touches => ({
62
+ x: touches.reduce((sum, t) => sum + t.pageX, 0) / touches.length,
63
+ y: touches.reduce((sum, t) => sum + t.pageY, 0) / touches.length
64
+ });
65
+ const spreadOf = touches => touches.length < 2 ? 0 : Math.hypot(touches[0].pageX - touches[1].pageX, touches[0].pageY - touches[1].pageY);
66
+ export function ZoomableImage({
67
+ uri,
68
+ accessibilityLabel,
69
+ onLoad,
70
+ onError
71
+ }) {
72
+ const reduceMotion = useReduceMotion();
73
+ const [scale] = useState(() => new Animated.Value(1));
74
+ const [x] = useState(() => new Animated.Value(0));
75
+ const [y] = useState(() => new Animated.Value(0));
76
+
77
+ // Gesture state, read by the (stable) PanResponder below.
78
+ const g = useRef({
79
+ view: {
80
+ scale: 1,
81
+ x: 0,
82
+ y: 0
83
+ },
84
+ box: {
85
+ width: 0,
86
+ height: 0
87
+ },
88
+ // Where the box sits on screen, so touches can be measured from its centre.
89
+ origin: {
90
+ x: 0,
91
+ y: 0
92
+ },
93
+ image: null,
94
+ // The view, finger centre and finger spread when the current set of fingers went down.
95
+ start: {
96
+ view: {
97
+ scale: 1,
98
+ x: 0,
99
+ y: 0
100
+ },
101
+ centre: {
102
+ x: 0,
103
+ y: 0
104
+ },
105
+ spread: 0,
106
+ count: 0
107
+ },
108
+ moved: false,
109
+ grantedAt: 0,
110
+ lastTap: null,
111
+ reduceMotion: false,
112
+ native: false
113
+ }).current;
114
+ g.reduceMotion = reduceMotion;
115
+ const boxRef = useRef(null);
116
+ const show = (view, animate) => {
117
+ g.view = view;
118
+ if (!animate || g.reduceMotion !== false) {
119
+ scale.setValue(view.scale);
120
+ x.setValue(view.x);
121
+ y.setValue(view.y);
122
+ return;
123
+ }
124
+ const spring = (value, toValue) => Animated.spring(value, {
125
+ toValue,
126
+ useNativeDriver: true,
127
+ bounciness: 0,
128
+ speed: 18
129
+ });
130
+ Animated.parallel([spring(scale, view.scale), spring(x, view.x), spring(y, view.y)]).start();
131
+ };
132
+
133
+ // Hands the transform to the native driver as soon as the image is on
134
+ // screen. Until a native animation had run, Android (new architecture) didn't
135
+ // draw finger moves, so pinching only worked after a double tap.
136
+ const goNative = () => {
137
+ if (g.native) return;
138
+ g.native = true;
139
+ const settle = (value, toValue) => Animated.spring(value, {
140
+ toValue,
141
+ useNativeDriver: true,
142
+ bounciness: 0,
143
+ speed: 18
144
+ });
145
+ Animated.parallel([settle(scale, g.view.scale), settle(x, g.view.x), settle(y, g.view.y)]).start();
146
+ };
147
+ const fromCentre = point => ({
148
+ x: point.x - g.origin.x - g.box.width / 2,
149
+ y: point.y - g.origin.y - g.box.height / 2
150
+ });
151
+ const content = () => fittedSize(g.box, g.image);
152
+ const begin = touches => {
153
+ g.start = {
154
+ view: g.view,
155
+ centre: fromCentre(centreOf(touches)),
156
+ spread: spreadOf(touches),
157
+ count: touches.length
158
+ };
159
+ };
160
+ const pan = useRef(PanResponder.create({
161
+ onStartShouldSetPanResponder: () => true,
162
+ onMoveShouldSetPanResponder: () => true,
163
+ // Never hand the gesture over: nothing else in the full-screen viewer
164
+ // wants it, and giving it up at 1× ended a pinch as it started.
165
+ onPanResponderTerminationRequest: () => false,
166
+ onPanResponderGrant: event => {
167
+ g.moved = false;
168
+ g.grantedAt = Date.now();
169
+ begin(event.nativeEvent.touches);
170
+ },
171
+ onPanResponderMove: (event, gesture) => {
172
+ const touches = event.nativeEvent.touches;
173
+ if (touches.length === 0) return;
174
+ // A finger landed or lifted: carry on from where the image is now.
175
+ if (touches.length !== g.start.count) begin(touches);
176
+ if (Math.abs(gesture.dx) > TAP_SLOP || Math.abs(gesture.dy) > TAP_SLOP || touches.length > 1) {
177
+ g.moved = true;
178
+ }
179
+ const centre = fromCentre(centreOf(touches));
180
+ if (touches.length > 1 && g.start.spread > 0) {
181
+ // Past the limits while pinching, then springs back on release.
182
+ const zoom = Math.min(MAX_ZOOM * 1.25, Math.max(0.75, g.start.view.scale * spreadOf(touches) / g.start.spread));
183
+ show(zoomAround(g.start.view, zoom, g.start.centre, centre), false);
184
+ } else if (g.start.view.scale > 1) {
185
+ // One finger drags the zoomed image; at 1× there's nothing to drag.
186
+ const dragged = {
187
+ ...g.start.view,
188
+ x: g.start.view.x + centre.x - g.start.centre.x,
189
+ y: g.start.view.y + centre.y - g.start.centre.y
190
+ };
191
+ show(clampView(dragged, g.box, content()), false);
192
+ }
193
+ },
194
+ onPanResponderRelease: event => {
195
+ const now = Date.now();
196
+ const {
197
+ pageX,
198
+ pageY
199
+ } = event.nativeEvent;
200
+ const isTap = !g.moved && now - g.grantedAt < DOUBLE_TAP_MS;
201
+ const last = g.lastTap;
202
+ if (isTap && last && now - last.at < DOUBLE_TAP_MS && Math.hypot(pageX - last.x, pageY - last.y) < DOUBLE_TAP_SLOP) {
203
+ g.lastTap = null;
204
+ // Zooming in keeps the tapped spot under the finger (as far as the edges allow).
205
+ const tapped = fromCentre({
206
+ x: pageX,
207
+ y: pageY
208
+ });
209
+ const target = g.view.scale > 1.01 ? {
210
+ scale: 1,
211
+ x: 0,
212
+ y: 0
213
+ } : zoomAround({
214
+ scale: 1,
215
+ x: 0,
216
+ y: 0
217
+ }, DOUBLE_TAP_ZOOM, tapped, tapped);
218
+ show(clampView(target, g.box, content()), true);
219
+ return;
220
+ }
221
+ g.lastTap = isTap ? {
222
+ at: now,
223
+ x: pageX,
224
+ y: pageY
225
+ } : null;
226
+ show(clampView(g.view, g.box, content()), true);
227
+ },
228
+ onPanResponderTerminate: () => show(clampView(g.view, g.box, content()), true)
229
+ })).current;
230
+ const onLayout = event => {
231
+ const {
232
+ width,
233
+ height
234
+ } = event.nativeEvent.layout;
235
+ g.box = {
236
+ width,
237
+ height
238
+ };
239
+ goNative();
240
+ boxRef.current?.measureInWindow?.((left, top) => {
241
+ g.origin = {
242
+ x: left,
243
+ y: top
244
+ };
245
+ });
246
+ };
247
+ const handleLoad = event => {
248
+ const source = event?.nativeEvent?.source;
249
+ if (source && source.width > 0 && source.height > 0) g.image = {
250
+ width: source.width,
251
+ height: source.height
252
+ };
253
+ onLoad();
254
+ };
255
+ return /*#__PURE__*/_jsx(View, {
256
+ ref: boxRef,
257
+ style: styles.box,
258
+ onLayout: onLayout,
259
+ ...pan.panHandlers,
260
+ children: /*#__PURE__*/_jsx(Animated.Image, {
261
+ source: {
262
+ uri
263
+ },
264
+ style: [styles.image, {
265
+ transform: [{
266
+ translateX: x
267
+ }, {
268
+ translateY: y
269
+ }, {
270
+ scale
271
+ }]
272
+ }],
273
+ resizeMode: "contain",
274
+ accessibilityLabel: accessibilityLabel,
275
+ accessibilityIgnoresInvertColors: true,
276
+ onLoad: handleLoad,
277
+ onError: onError
278
+ })
279
+ });
280
+ }
281
+ const styles = StyleSheet.create({
282
+ box: {
283
+ flex: 1,
284
+ overflow: 'hidden'
285
+ },
286
+ image: {
287
+ width: '100%',
288
+ height: '100%'
289
+ }
290
+ });
291
+ //# sourceMappingURL=ZoomableImage.js.map