show progress during upload
More explicit feedback of the state of their upload. Also prevents recording more media at the same time or interacting with the stream which would feel stale to the human since they just created something and it's yet to be present in the stream.
This commit is contained in:
@@ -36,7 +36,12 @@ type ComposeUiState =
|
|||||||
uri: string;
|
uri: string;
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
}
|
}
|
||||||
| { kind: "uploading" };
|
| {
|
||||||
|
kind: "uploading";
|
||||||
|
mode: RecordingMode;
|
||||||
|
uri: string;
|
||||||
|
durationMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
interface SubmitMediaParams {
|
interface SubmitMediaParams {
|
||||||
fileUri: string;
|
fileUri: string;
|
||||||
@@ -125,7 +130,12 @@ export function ComposeDock({
|
|||||||
const sendReview = useEvent(async () => {
|
const sendReview = useEvent(async () => {
|
||||||
if (ui.kind !== "review" || !userId) return;
|
if (ui.kind !== "review" || !userId) return;
|
||||||
const captured = ui;
|
const captured = ui;
|
||||||
setUi({ kind: "uploading" });
|
setUi({
|
||||||
|
kind: "uploading",
|
||||||
|
mode: captured.mode,
|
||||||
|
uri: captured.uri,
|
||||||
|
durationMs: captured.durationMs,
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const mimeType =
|
const mimeType =
|
||||||
captured.mode === "audio" ? "audio/mp4" : "video/mp4";
|
captured.mode === "audio" ? "audio/mp4" : "video/mp4";
|
||||||
@@ -251,10 +261,17 @@ export function ComposeDock({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<ReviewSheet
|
<ReviewSheet
|
||||||
open={ui.kind === "review"}
|
open={ui.kind === "review" || ui.kind === "uploading"}
|
||||||
uri={ui.kind === "review" ? ui.uri : null}
|
uri={
|
||||||
mode={ui.kind === "review" ? ui.mode : null}
|
ui.kind === "review" || ui.kind === "uploading" ? ui.uri : null
|
||||||
durationMs={ui.kind === "review" ? ui.durationMs : 0}
|
}
|
||||||
|
mode={
|
||||||
|
ui.kind === "review" || ui.kind === "uploading" ? ui.mode : null
|
||||||
|
}
|
||||||
|
durationMs={
|
||||||
|
ui.kind === "review" || ui.kind === "uploading" ? ui.durationMs : 0
|
||||||
|
}
|
||||||
|
sending={ui.kind === "uploading"}
|
||||||
onSend={sendReview}
|
onSend={sendReview}
|
||||||
onRetake={retake}
|
onRetake={retake}
|
||||||
onCancel={cancelReview}
|
onCancel={cancelReview}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect } from "react";
|
||||||
import { Modal, Pressable, Text, View } from "react-native";
|
import { ActivityIndicator, Modal, Pressable, Text, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
initialWindowMetrics,
|
initialWindowMetrics,
|
||||||
SafeAreaProvider,
|
SafeAreaProvider,
|
||||||
@@ -17,6 +17,12 @@ interface ReviewSheetProps {
|
|||||||
uri: string | null;
|
uri: string | null;
|
||||||
mode: "video" | "audio" | null;
|
mode: "video" | "audio" | null;
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
|
/**
|
||||||
|
* True once the parent has flipped to the uploading state. The sheet stays
|
||||||
|
* mounted (so playback doesn't tear down) and blocks all interaction behind
|
||||||
|
* a "Sending..." overlay until the parent clears it.
|
||||||
|
*/
|
||||||
|
sending: boolean;
|
||||||
onSend: () => Promise<void>;
|
onSend: () => Promise<void>;
|
||||||
onRetake: () => void;
|
onRetake: () => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -32,6 +38,7 @@ export function ReviewSheet({
|
|||||||
uri,
|
uri,
|
||||||
mode,
|
mode,
|
||||||
durationMs,
|
durationMs,
|
||||||
|
sending,
|
||||||
onSend,
|
onSend,
|
||||||
onRetake,
|
onRetake,
|
||||||
onCancel,
|
onCancel,
|
||||||
@@ -41,22 +48,22 @@ export function ReviewSheet({
|
|||||||
p.muted = false;
|
p.muted = false;
|
||||||
p.audioMixingMode = "mixWithOthers";
|
p.audioMixingMode = "mixWithOthers";
|
||||||
});
|
});
|
||||||
const [submitting, setSubmitting] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open && uri) {
|
if (!open || !uri) return;
|
||||||
|
if (sending) {
|
||||||
|
player.pause();
|
||||||
|
} else {
|
||||||
player.play();
|
player.play();
|
||||||
}
|
}
|
||||||
}, [open, uri, player]);
|
}, [open, uri, sending, player]);
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (submitting) return;
|
if (sending) return;
|
||||||
setSubmitting(true);
|
|
||||||
try {
|
try {
|
||||||
await onSend();
|
await onSend();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(toUserMessage(err));
|
toast.error(toUserMessage(err));
|
||||||
setSubmitting(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,7 +74,7 @@ export function ReviewSheet({
|
|||||||
visible={open}
|
visible={open}
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
transparent={false}
|
transparent={false}
|
||||||
onRequestClose={onCancel}
|
onRequestClose={sending ? undefined : onCancel}
|
||||||
>
|
>
|
||||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||||
<View className="flex-1 bg-black">
|
<View className="flex-1 bg-black">
|
||||||
@@ -110,10 +117,18 @@ export function ReviewSheet({
|
|||||||
<View className="px-4 pt-3">
|
<View className="px-4 pt-3">
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onCancel}
|
onPress={onCancel}
|
||||||
|
disabled={sending}
|
||||||
hitSlop={12}
|
hitSlop={12}
|
||||||
accessibilityLabel="Cancel"
|
accessibilityLabel="Cancel"
|
||||||
>
|
>
|
||||||
<Text className="text-white/80 text-base">Cancel</Text>
|
<Text
|
||||||
|
className={cn(
|
||||||
|
"text-base",
|
||||||
|
sending ? "text-white/30" : "text-white/80",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
@@ -125,8 +140,11 @@ export function ReviewSheet({
|
|||||||
<View className="flex-row items-center justify-between px-6 pb-4 pt-3">
|
<View className="flex-row items-center justify-between px-6 pb-4 pt-3">
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onRetake}
|
onPress={onRetake}
|
||||||
disabled={submitting}
|
disabled={sending}
|
||||||
className="rounded-full bg-white/15 px-5 py-3"
|
className={cn(
|
||||||
|
"rounded-full bg-white/15 px-5 py-3",
|
||||||
|
sending && "opacity-40",
|
||||||
|
)}
|
||||||
accessibilityLabel="Retake"
|
accessibilityLabel="Retake"
|
||||||
>
|
>
|
||||||
<Text className="text-white text-base font-medium">Retake</Text>
|
<Text className="text-white text-base font-medium">Retake</Text>
|
||||||
@@ -134,19 +152,26 @@ export function ReviewSheet({
|
|||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleSend}
|
onPress={handleSend}
|
||||||
disabled={submitting}
|
disabled={sending}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-full px-7 py-3",
|
"rounded-full px-7 py-3",
|
||||||
submitting ? "bg-white/40" : "bg-white",
|
sending ? "bg-white/40" : "bg-white",
|
||||||
)}
|
)}
|
||||||
accessibilityLabel="Send"
|
accessibilityLabel="Send"
|
||||||
>
|
>
|
||||||
<Text className="text-black text-base font-semibold">
|
<Text className="text-black text-base font-semibold">
|
||||||
{submitting ? "Sending..." : "Send"}
|
{sending ? "Sending..." : "Send"}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
||||||
|
{sending ? (
|
||||||
|
<View className="absolute inset-0 items-center justify-center bg-black/85">
|
||||||
|
<ActivityIndicator color="white" />
|
||||||
|
<Text className="text-white/70 mt-4 text-sm">Sending...</Text>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
Reference in New Issue
Block a user