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;
|
||||
durationMs: number;
|
||||
}
|
||||
| { kind: "uploading" };
|
||||
| {
|
||||
kind: "uploading";
|
||||
mode: RecordingMode;
|
||||
uri: string;
|
||||
durationMs: number;
|
||||
};
|
||||
|
||||
interface SubmitMediaParams {
|
||||
fileUri: string;
|
||||
@@ -125,7 +130,12 @@ export function ComposeDock({
|
||||
const sendReview = useEvent(async () => {
|
||||
if (ui.kind !== "review" || !userId) return;
|
||||
const captured = ui;
|
||||
setUi({ kind: "uploading" });
|
||||
setUi({
|
||||
kind: "uploading",
|
||||
mode: captured.mode,
|
||||
uri: captured.uri,
|
||||
durationMs: captured.durationMs,
|
||||
});
|
||||
try {
|
||||
const mimeType =
|
||||
captured.mode === "audio" ? "audio/mp4" : "video/mp4";
|
||||
@@ -251,10 +261,17 @@ export function ComposeDock({
|
||||
) : null}
|
||||
|
||||
<ReviewSheet
|
||||
open={ui.kind === "review"}
|
||||
uri={ui.kind === "review" ? ui.uri : null}
|
||||
mode={ui.kind === "review" ? ui.mode : null}
|
||||
durationMs={ui.kind === "review" ? ui.durationMs : 0}
|
||||
open={ui.kind === "review" || ui.kind === "uploading"}
|
||||
uri={
|
||||
ui.kind === "review" || ui.kind === "uploading" ? ui.uri : null
|
||||
}
|
||||
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}
|
||||
onRetake={retake}
|
||||
onCancel={cancelReview}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Modal, Pressable, Text, View } from "react-native";
|
||||
import { useEffect } from "react";
|
||||
import { ActivityIndicator, Modal, Pressable, Text, View } from "react-native";
|
||||
import {
|
||||
initialWindowMetrics,
|
||||
SafeAreaProvider,
|
||||
@@ -17,6 +17,12 @@ interface ReviewSheetProps {
|
||||
uri: string | null;
|
||||
mode: "video" | "audio" | null;
|
||||
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>;
|
||||
onRetake: () => void;
|
||||
onCancel: () => void;
|
||||
@@ -32,6 +38,7 @@ export function ReviewSheet({
|
||||
uri,
|
||||
mode,
|
||||
durationMs,
|
||||
sending,
|
||||
onSend,
|
||||
onRetake,
|
||||
onCancel,
|
||||
@@ -41,22 +48,22 @@ export function ReviewSheet({
|
||||
p.muted = false;
|
||||
p.audioMixingMode = "mixWithOthers";
|
||||
});
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open && uri) {
|
||||
if (!open || !uri) return;
|
||||
if (sending) {
|
||||
player.pause();
|
||||
} else {
|
||||
player.play();
|
||||
}
|
||||
}, [open, uri, player]);
|
||||
}, [open, uri, sending, player]);
|
||||
|
||||
const handleSend = async () => {
|
||||
if (submitting) return;
|
||||
setSubmitting(true);
|
||||
if (sending) return;
|
||||
try {
|
||||
await onSend();
|
||||
} catch (err) {
|
||||
toast.error(toUserMessage(err));
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,7 +74,7 @@ export function ReviewSheet({
|
||||
visible={open}
|
||||
animationType="fade"
|
||||
transparent={false}
|
||||
onRequestClose={onCancel}
|
||||
onRequestClose={sending ? undefined : onCancel}
|
||||
>
|
||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||
<View className="flex-1 bg-black">
|
||||
@@ -110,10 +117,18 @@ export function ReviewSheet({
|
||||
<View className="px-4 pt-3">
|
||||
<Pressable
|
||||
onPress={onCancel}
|
||||
disabled={sending}
|
||||
hitSlop={12}
|
||||
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>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
@@ -125,8 +140,11 @@ export function ReviewSheet({
|
||||
<View className="flex-row items-center justify-between px-6 pb-4 pt-3">
|
||||
<Pressable
|
||||
onPress={onRetake}
|
||||
disabled={submitting}
|
||||
className="rounded-full bg-white/15 px-5 py-3"
|
||||
disabled={sending}
|
||||
className={cn(
|
||||
"rounded-full bg-white/15 px-5 py-3",
|
||||
sending && "opacity-40",
|
||||
)}
|
||||
accessibilityLabel="Retake"
|
||||
>
|
||||
<Text className="text-white text-base font-medium">Retake</Text>
|
||||
@@ -134,19 +152,26 @@ export function ReviewSheet({
|
||||
|
||||
<Pressable
|
||||
onPress={handleSend}
|
||||
disabled={submitting}
|
||||
disabled={sending}
|
||||
className={cn(
|
||||
"rounded-full px-7 py-3",
|
||||
submitting ? "bg-white/40" : "bg-white",
|
||||
sending ? "bg-white/40" : "bg-white",
|
||||
)}
|
||||
accessibilityLabel="Send"
|
||||
>
|
||||
<Text className="text-black text-base font-semibold">
|
||||
{submitting ? "Sending..." : "Send"}
|
||||
{sending ? "Sending..." : "Send"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
</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>
|
||||
</SafeAreaProvider>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user