Files
Luke Gustafson 6c40d2f0cc v1.4.0 (#31)
* fix: register physical Tab key on iPadOS/iOS hardware keyboards (#14)

The native module only registered Shift+Tab but not bare Tab, so iOS
intercepted it for UI focus navigation. Register an unmodified Tab
UIKeyCommand with wantsPriorityOverSystemBehavior and handle both
Tab and Shift+Tab in the same JS branch.

Fixes Termix-SSH/Support#557

* fix: pass jumpHosts, forceKeyboardInteractive, and overrideCredentialUsername to terminal connection (#15)

These fields were defined on SSHHost but omitted from the terminal
hostConfig passthrough. Without jumpHosts the backend cannot route
through jump servers; without forceKeyboardInteractive hosts that
require keyboard-interactive auth fail silently; without
overrideCredentialUsername shared credentials use the wrong username.

Fixes Termix-SSH/Support#422
Fixes Termix-SSH/Support#638

* fix: improve mobile terminal scroll recovery (#17)

* Add mobile remote desktop sessions (#18)

* feat: add mobile remote desktop sessions

* feat: add remote desktop input controls

* feat: polish remote desktop controls

* feat: add mobile terminal font selection (#19)

* feat: add mobile shift tab hotkey (#20)

* feat: initial UI redesign

* feat: revamp server login system

* chore: update app icon/background color

* fix: remove unused fields in the settings tab

* feat: improve the user pin system and update all settings modals to use new ui

* fix: active server not updating unless app restarts

* feat: add version in settings

* feat: improve host page UI (updated host form and added credential management)

* fix: preserve composed iOS terminal input

* ci: restore mobile checks

* feat: rewrite of connection system for all types

* fix: preserve mobile jump hosts (#30)

* fix: guard android user ca trust (#29)

* fix: capture ios hardware tab key (#27)

* fix: reset terminal input after special keys (#28)

* fix: open oidc in system browser (#26)

* fix: preserve websocket auth context (#25)

* fix: keep none-auth terminal prompts alive (#23)

* fix: allow local network SSL in Android app (#21)

* feat: allow oidc passkeys

* feat: bundeled xterm into app and added docker loading screen

* feat: add 2fa, api key, and active sessions to settings and addressed multiple bug fixes and inconsistencies with termix web

* chore: update repo images and readme

* chore: update readme to split table for screenshots

* chore: rename ios to apple in build workflow

* fix: android build failure

* feat: add snippet management and fix several bugs

* fix: web login and oidc incorrect logic

* chore: run linter

---------

Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-03 16:49:50 -05:00

157 lines
4.8 KiB
TypeScript

import { View, ActivityIndicator, ScrollView } from "react-native";
import { AlertCircle, RotateCcw } from "lucide-react-native";
import { Text, Button } from "@/app/components/ui";
import { useThemeColor } from "@/app/contexts/ThemeContext";
import { ConnectionLog } from "./useConnectionLog";
import type { ConnectionLogEntry } from "@/types";
export type SessionFrameStatus =
| "loading"
| "ready"
| "error"
| "empty";
/**
* SessionFrame — the standard chrome shared by every session type (file
* manager, docker, stats, tunnel). Renders an optional header (title/subtitle +
* trailing actions), a connection-log panel, and centralized loading / error /
* empty states so each session type doesn't reinvent them.
*
* The terminal and remote-desktop canvases are full-bleed and manage their own
* chrome, so they don't use this.
*/
export function SessionFrame({
title,
subtitle,
headerActions,
toolbar,
status = "ready",
loadingLabel = "Connecting…",
errorMessage,
emptyMessage = "Nothing to show",
emptyIcon,
onRetry,
logEntries,
isConnecting,
isConnected,
hasConnectionError,
onLogClear,
scroll = false,
children,
}: {
title?: string;
subtitle?: string;
headerActions?: React.ReactNode;
toolbar?: React.ReactNode;
status?: SessionFrameStatus;
loadingLabel?: string;
errorMessage?: string;
emptyMessage?: string;
emptyIcon?: React.ReactNode;
onRetry?: () => void;
logEntries?: ConnectionLogEntry[];
isConnecting?: boolean;
isConnected?: boolean;
hasConnectionError?: boolean;
onLogClear?: () => void;
scroll?: boolean;
children?: React.ReactNode;
}) {
const color = useThemeColor();
const Body: any = scroll ? ScrollView : View;
const bodyProps = scroll
? { contentContainerStyle: { flexGrow: 1 } }
: { className: "flex-1" };
return (
<View className="flex-1 bg-background">
{(title || headerActions) && (
<View className="flex-row items-center gap-2 px-4 py-3 border-b border-border">
<View className="flex-1 min-w-0">
{title ? (
<Text
weight="bold"
className="text-base text-foreground"
numberOfLines={1}
>
{title}
</Text>
) : null}
{subtitle ? (
<Text
className="text-xs text-muted-foreground"
numberOfLines={1}
>
{subtitle}
</Text>
) : null}
</View>
{headerActions ? (
<View className="flex-row items-center gap-1.5">{headerActions}</View>
) : null}
</View>
)}
{toolbar ? (
<View className="border-b border-border">{toolbar}</View>
) : null}
{/* Connection log overlay — covers the frame while connecting/errored */}
{logEntries !== undefined && onLogClear !== undefined ? (
<ConnectionLog
entries={logEntries}
isConnecting={isConnecting ?? false}
isConnected={isConnected ?? false}
hasConnectionError={hasConnectionError ?? false}
onClear={onLogClear}
/>
) : null}
{/* Suppress the loading spinner when the log overlay is covering the screen */}
{status === "loading" && !(logEntries && logEntries.length > 0 && !isConnected) ? (
<View className="flex-1 items-center justify-center gap-3">
<ActivityIndicator size="large" color={color("accent-brand")} />
<Text className="text-sm text-muted-foreground">{loadingLabel}</Text>
</View>
) : status === "error" ? (
<View className="flex-1 items-center justify-center px-8 gap-3">
<AlertCircle size={36} color={color("destructive")} />
<Text className="text-sm text-muted-foreground text-center leading-5">
{errorMessage || "Something went wrong"}
</Text>
{onRetry ? (
<Button
variant="outline"
size="sm"
icon={<RotateCcw size={14} color={color("foreground")} />}
onPress={onRetry}
>
Retry
</Button>
) : null}
</View>
) : status === "empty" ? (
<View className="flex-1 items-center justify-center px-8 gap-3">
{emptyIcon ?? <AlertCircle size={32} color={color("muted-foreground")} />}
<Text className="text-sm text-muted-foreground text-center">
{emptyMessage}
</Text>
{onRetry ? (
<Button
variant="ghost"
size="sm"
icon={<RotateCcw size={14} color={color("foreground")} />}
onPress={onRetry}
>
Refresh
</Button>
) : null}
</View>
) : (
<Body {...bodyProps}>{children}</Body>
)}
</View>
);
}