evOo-local/apps/mobile/components/computer-update-progress.tsx
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

197 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { COMPUTER_UPDATE_STAGES, type ComputerUpdate } from "@rakazo/contracts";
import { computerUpdateNeedsAttention, computerUpdateStages } from "@rakazo/core";
import { usePathname } from "expo-router";
import { useEffect, useState, useSyncExternalStore } from "react";
import { ActivityIndicator, Alert, Modal, Pressable, StyleSheet, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { computerUpdates } from "../lib/computer-updates";
import { useI18n } from "../lib/i18n";
import { useMobileTokens } from "../lib/native";
export function ComputerUpdateProgress() {
const { t } = useI18n();
const tokens = useMobileTokens();
const insets = useSafeAreaInsets();
const pathname = usePathname();
const { updates, openId } = useSyncExternalStore(
computerUpdates.subscribe,
computerUpdates.getSnapshot,
);
const [error, setError] = useState(false);
const [busy, setBusy] = useState(false);
const authenticated = !["/sign-in"].includes(pathname);
useEffect(() => (authenticated ? computerUpdates.watch() : undefined), [authenticated]);
const labels = [
t("Getting ready"),
t("Saving your workspace"),
t("Recreating the computer"),
t("Restoring your workspace"),
t("Reconnecting"),
];
const title = (update: ComputerUpdate) =>
computerUpdateNeedsAttention(update)
? update.action === "recover"
? t("Recovery failed")
: t("Update failed")
: update.action === "recover"
? update.mode === "team"
? t("Recovering Team Computer")
: t("Recovering {name}’s Computer", { name: update.name })
: update.mode === "team"
? t("Updating Team Computer")
: t("Updating {name}’s Computer", { name: update.name });
const selected = updates.find((item) => item.id === openId);
if (!authenticated) return null;
return (
<>
<View pointerEvents="box-none" style={[styles.pills, { top: insets.top + 8 }]}>
{updates.map((update) => (
<Pressable
key={update.id}
accessibilityRole="button"
onPress={() => {
setError(false);
computerUpdates.open(update.id);
}}
style={[styles.pill, { backgroundColor: tokens.card, borderColor: tokens.border }]}
>
{!computerUpdateNeedsAttention(update) ? (
<ActivityIndicator color={tokens.foreground} />
) : null}
<View>
<Text style={{ color: tokens.foreground }}>{title(update)}</Text>
<Text style={[styles.secondary, { color: tokens.mutedForeground }]}>
{labels[COMPUTER_UPDATE_STAGES.indexOf(update.stage)]}
</Text>
</View>
</Pressable>
))}
</View>
<Modal
visible={Boolean(selected)}
presentationStyle="pageSheet"
animationType="slide"
onRequestClose={() => computerUpdates.open(null)}
>
{selected ? (
<View style={[styles.sheet, { backgroundColor: tokens.background }]}>
<Text accessibilityRole="header" style={[styles.title, { color: tokens.foreground }]}>
{title(selected)}
</Text>
{computerUpdateNeedsAttention(selected) ? (
<Text style={{ color: tokens.mutedForeground }}>
{selected.status === "interrupted"
? t("Recovery is unavailable until the previous operation has stopped.")
: t("Recovery restores the last saved workspace. Unsaved work may be lost.")}
</Text>
) : (
computerUpdateStages(selected.action).map((stage, index) => {
const current = computerUpdateStages(selected.action).indexOf(selected.stage);
return (
<View key={stage} style={styles.step}>
{index === current ? (
<ActivityIndicator color={tokens.foreground} />
) : (
<Text style={{ color: tokens.mutedForeground }}>
{index < current ? "✓" : "○"}
</Text>
)}
<Text
accessibilityLiveRegion={index === current ? "polite" : "none"}
style={{
color: index === current ? tokens.foreground : tokens.mutedForeground,
}}
>
{labels[COMPUTER_UPDATE_STAGES.indexOf(stage)]}
</Text>
</View>
);
})
)}
{error ? (
<Text accessibilityRole="alert" style={{ color: tokens.destructive }}>
{t("Could not complete action")}
</Text>
) : null}
{selected.status === "interrupted" && selected.canReleaseReservation ? (
<Pressable
accessibilityRole="button"
disabled={busy}
style={styles.button}
onPress={() =>
Alert.alert(
t("Release interrupted computer?"),
t("Make sure nothing is still running on this computer."),
[
{ text: t("Cancel"), style: "cancel" },
{
text: t("Nothing is still running"),
onPress: () => {
setBusy(true);
setError(false);
void computerUpdates
.releaseInterrupted(selected.id)
.catch(() => setError(true))
.finally(() => setBusy(false));
},
},
],
)
}
>
<Text style={{ color: tokens.foreground }}>{t("Release computer")}</Text>
</Pressable>
) : null}
{selected.status === "failed" ? (
<Pressable
accessibilityRole="button"
disabled={busy}
onPress={() => {
setBusy(true);
setError(false);
void computerUpdates
.start(selected.botId, "recover")
.catch(() => setError(true))
.finally(() => setBusy(false));
}}
style={styles.button}
>
<Text style={{ color: tokens.foreground }}>{t("Recover computer")}</Text>
</Pressable>
) : null}
<Pressable
accessibilityRole="button"
style={styles.button}
onPress={() => {
if (selected.status === "failed")
void computerUpdates.dismiss(selected.id).catch(() => setError(true));
else computerUpdates.open(null);
}}
>
<Text style={{ color: tokens.foreground }}>
{selected.status === "failed" ? t("Dismiss") : t("Continue in Background")}
</Text>
</Pressable>
</View>
) : null}
</Modal>
</>
);
}
const styles = StyleSheet.create({
pills: { position: "absolute", left: 16, right: 16, zIndex: 100, alignItems: "center", gap: 8 },
pill: {
flexDirection: "row",
alignItems: "center",
gap: 12,
borderWidth: 1,
borderRadius: 14,
padding: 12,
},
secondary: { fontSize: 12, textAlign: "center", marginTop: 3 },
sheet: { flex: 1, padding: 24, gap: 24 },
title: { fontSize: 22, fontWeight: "600" },
step: { flexDirection: "row", alignItems: "center", gap: 16 },
button: { minHeight: 44, justifyContent: "center", alignItems: "center" },
});