@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.
- package/CHANGELOG.md +69 -0
- package/README.md +40 -3
- package/android/build.gradle +22 -0
- package/android/src/main/java/expo/modules/repzowidget/RepzoVoicePlayer.kt +222 -0
- package/android/src/main/java/expo/modules/repzowidget/RepzoWidgetAudioModule.kt +68 -0
- package/expo-module.config.json +9 -0
- package/ios/RepzoVoicePlayer.swift +175 -0
- package/ios/RepzoWidgetAudio.podspec +26 -0
- package/ios/RepzoWidgetAudioModule.swift +65 -0
- package/lib/commonjs/api/client.js +47 -2
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/audio/native-player.js +97 -0
- package/lib/commonjs/audio/native-player.js.map +1 -0
- package/lib/commonjs/audio/voice.js +146 -0
- package/lib/commonjs/audio/voice.js.map +1 -0
- package/lib/commonjs/i18n.js +6 -0
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +4 -0
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/use-widget-state.js +31 -0
- package/lib/commonjs/state/use-widget-state.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +44 -1
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +7 -3
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/FileCard.js +134 -0
- package/lib/commonjs/ui/components/FileCard.js.map +1 -0
- package/lib/commonjs/ui/components/ImageViewer.js +17 -35
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
- package/lib/commonjs/ui/components/Launcher.js +15 -1
- package/lib/commonjs/ui/components/Launcher.js.map +1 -1
- package/lib/commonjs/ui/components/MessagesSkeleton.js +126 -0
- package/lib/commonjs/ui/components/MessagesSkeleton.js.map +1 -0
- package/lib/commonjs/ui/components/VoiceNote.js +353 -0
- package/lib/commonjs/ui/components/VoiceNote.js.map +1 -0
- package/lib/commonjs/ui/components/ZoomableImage.js +299 -0
- package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -0
- package/lib/commonjs/ui/components/icons.js +57 -0
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.js +39 -18
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +5 -16
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/api/client.js +48 -2
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/audio/native-player.js +93 -0
- package/lib/module/audio/native-player.js.map +1 -0
- package/lib/module/audio/voice.js +136 -0
- package/lib/module/audio/voice.js.map +1 -0
- package/lib/module/i18n.js +6 -0
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +4 -0
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/use-widget-state.js +30 -1
- package/lib/module/state/use-widget-state.js.map +1 -1
- package/lib/module/state/widget-store.js +44 -1
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +7 -3
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/FileCard.js +130 -0
- package/lib/module/ui/components/FileCard.js.map +1 -0
- package/lib/module/ui/components/ImageViewer.js +18 -36
- package/lib/module/ui/components/ImageViewer.js.map +1 -1
- package/lib/module/ui/components/Launcher.js +16 -2
- package/lib/module/ui/components/Launcher.js.map +1 -1
- package/lib/module/ui/components/MessagesSkeleton.js +124 -0
- package/lib/module/ui/components/MessagesSkeleton.js.map +1 -0
- package/lib/module/ui/components/VoiceNote.js +349 -0
- package/lib/module/ui/components/VoiceNote.js.map +1 -0
- package/lib/module/ui/components/ZoomableImage.js +291 -0
- package/lib/module/ui/components/ZoomableImage.js.map +1 -0
- package/lib/module/ui/components/icons.js +55 -0
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.js +39 -18
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +5 -16
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/typescript/commonjs/api/client.d.ts +12 -0
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/audio/native-player.d.ts +36 -0
- package/lib/typescript/commonjs/audio/native-player.d.ts.map +1 -0
- package/lib/typescript/commonjs/audio/voice.d.ts +42 -0
- package/lib/typescript/commonjs/audio/voice.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n.d.ts +4 -0
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/use-widget-state.d.ts +8 -0
- package/lib/typescript/commonjs/state/use-widget-state.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +13 -0
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/FileCard.d.ts +21 -0
- package/lib/typescript/commonjs/ui/components/FileCard.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +2 -2
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/Launcher.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts +10 -0
- package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts +32 -0
- package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +45 -0
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts +4 -0
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +4 -16
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +4 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/api/client.d.ts +12 -0
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/audio/native-player.d.ts +36 -0
- package/lib/typescript/module/audio/native-player.d.ts.map +1 -0
- package/lib/typescript/module/audio/voice.d.ts +42 -0
- package/lib/typescript/module/audio/voice.d.ts.map +1 -0
- package/lib/typescript/module/i18n.d.ts +4 -0
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/use-widget-state.d.ts +8 -0
- package/lib/typescript/module/state/use-widget-state.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +13 -0
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/FileCard.d.ts +21 -0
- package/lib/typescript/module/ui/components/FileCard.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ImageViewer.d.ts +2 -2
- package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/Launcher.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts +10 -0
- package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/VoiceNote.d.ts +32 -0
- package/lib/typescript/module/ui/components/VoiceNote.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts +45 -0
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/icons.d.ts +4 -0
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +4 -16
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +4 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/package.json +14 -3
- package/react-native.config.js +12 -0
- package/src/api/client.ts +56 -2
- package/src/audio/native-player.ts +106 -0
- package/src/audio/voice.ts +127 -0
- package/src/i18n.ts +6 -0
- package/src/locales.ts +4 -0
- package/src/state/use-widget-state.ts +30 -1
- package/src/state/widget-store.ts +45 -1
- package/src/ui/WidgetProvider.tsx +7 -3
- package/src/ui/components/FileCard.tsx +111 -0
- package/src/ui/components/ImageViewer.tsx +11 -30
- package/src/ui/components/Launcher.tsx +7 -2
- package/src/ui/components/MessagesSkeleton.tsx +92 -0
- package/src/ui/components/VoiceNote.tsx +355 -0
- package/src/ui/components/ZoomableImage.tsx +250 -0
- package/src/ui/components/icons.tsx +33 -0
- package/src/ui/screens/chat/ChatView.styles.ts +5 -16
- package/src/ui/screens/chat/ChatView.tsx +43 -16
- 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
|