refactor: migrate workspace tab actions to keyboard action dispatcher

This commit is contained in:
Mohamed Boudra
2026-03-17 20:19:41 +07:00
parent 4897627943
commit abc8ad3fd4
18 changed files with 322 additions and 649 deletions
@@ -0,0 +1,180 @@
import { useEffect, useRef, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { X } from "lucide-react-native";
import { useDesktopAppUpdater } from "@/desktop/updates/use-desktop-app-updater";
import { openExternalUrl } from "@/utils/open-external-url";
const CHECK_INTERVAL_MS = 30 * 60 * 1000;
const CHANGELOG_URL = "https://paseo.sh/changelog";
export function UpdateBanner() {
const { theme } = useUnistyles();
const {
isDesktop,
status,
availableUpdate,
checkForUpdates,
installUpdate,
isInstalling,
} = useDesktopAppUpdater();
const [dismissed, setDismissed] = useState(false);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (!isDesktop) return;
void checkForUpdates({ silent: true });
intervalRef.current = setInterval(() => {
void checkForUpdates({ silent: true });
}, CHECK_INTERVAL_MS);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, [isDesktop, checkForUpdates]);
if (!isDesktop) return null;
if (dismissed) return null;
if (status !== "available" && status !== "installed") return null;
const isInstalled = status === "installed";
return (
<View style={styles.container} pointerEvents="box-none">
<View style={styles.banner}>
<Pressable
onPress={() => setDismissed(true)}
hitSlop={8}
style={styles.closeButton}
>
<X size={14} color={theme.colors.foregroundMuted} />
</Pressable>
<View style={styles.textSection}>
<Text style={styles.title}>
{isInstalled ? "Update installed" : "Update available"}
</Text>
<Text style={styles.subtitle}>
{isInstalled
? "Restart to use the new version."
: `${availableUpdate?.latestVersion ? `v${availableUpdate.latestVersion.replace(/^v/i, "")} is ready` : "A new version is ready"} to install.`}
</Text>
</View>
<View style={styles.actions}>
<Pressable
onPress={() => void openExternalUrl(CHANGELOG_URL)}
style={({ pressed }) => [
styles.outlineButton,
pressed && styles.buttonPressed,
]}
>
<Text style={styles.outlineButtonText}>What's new</Text>
</Pressable>
{!isInstalled && (
<Pressable
onPress={() => void installUpdate()}
disabled={isInstalling}
style={({ pressed }) => [
styles.primaryButton,
pressed && styles.buttonPressed,
isInstalling && styles.buttonDisabled,
]}
>
<Text style={styles.primaryButtonText}>
{isInstalling ? "Installing..." : "Install & restart"}
</Text>
</Pressable>
)}
</View>
</View>
</View>
);
}
const styles = StyleSheet.create((theme) => ({
container: {
position: "absolute",
bottom: theme.spacing[4],
right: theme.spacing[4],
zIndex: 1000,
},
banner: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[4],
backgroundColor: theme.colors.surface2,
borderRadius: theme.borderRadius.xl,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
paddingVertical: theme.spacing[3],
paddingLeft: theme.spacing[4],
paddingRight: theme.spacing[3],
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 12,
elevation: 8,
maxWidth: 480,
},
closeButton: {
position: "absolute",
top: theme.spacing[2],
left: theme.spacing[2],
padding: theme.spacing[1],
zIndex: 1,
},
textSection: {
flex: 1,
gap: 2,
paddingTop: theme.spacing[1],
},
title: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
fontWeight: theme.fontWeight.semibold,
},
subtitle: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
},
actions: {
flexDirection: "row",
gap: theme.spacing[2],
alignItems: "center",
},
outlineButton: {
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.md,
borderWidth: theme.borderWidth[1],
borderColor: theme.colors.border,
},
outlineButtonText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.medium,
},
primaryButton: {
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
borderRadius: theme.borderRadius.md,
backgroundColor: theme.colors.foreground,
},
primaryButtonText: {
color: theme.colors.surface0,
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.medium,
},
buttonPressed: {
opacity: 0.8,
},
buttonDisabled: {
opacity: 0.5,
},
}));