From 9433a2816d50f933c0d3eefb4eee2d86a3e26aa2 Mon Sep 17 00:00:00 2001 From: EthanShoeDev <13422990+EthanShoeDev@users.noreply.github.com> Date: Mon, 22 Sep 2025 17:40:12 -0400 Subject: [PATCH] improve detail screen --- apps/mobile/src/app/(tabs)/index.tsx | 7 +- apps/mobile/src/app/(tabs)/shell/detail.tsx | 242 +++++++++--------- apps/mobile/src/app/(test)/_layout.tsx | 1 + .../src/app/(test)/toolbar-example/index.tsx | 30 ++- apps/mobile/src/app/index.tsx | 2 +- apps/mobile/src/lib/useBottomTabPadding.ts | 36 --- apps/mobile/src/lib/useBottomTabSpacing.ts | 12 + .../src-internal/main.tsx | 2 +- 8 files changed, 157 insertions(+), 175 deletions(-) delete mode 100644 apps/mobile/src/lib/useBottomTabPadding.ts create mode 100644 apps/mobile/src/lib/useBottomTabSpacing.ts diff --git a/apps/mobile/src/app/(tabs)/index.tsx b/apps/mobile/src/app/(tabs)/index.tsx index 569e6c4..4e3fb78 100644 --- a/apps/mobile/src/app/(tabs)/index.tsx +++ b/apps/mobile/src/app/(tabs)/index.tsx @@ -21,7 +21,7 @@ import { type InputConnectionDetails, } from '@/lib/secrets-manager'; import { useTheme } from '@/lib/theme'; -import { useBottomTabPadding } from '@/lib/useBottomTabPadding'; +import { useBottomTabSpacing } from '@/lib/useBottomTabSpacing'; export default function TabsIndex() { return ; @@ -45,7 +45,7 @@ function Host() { const sshConnMutation = useSshConnMutation({ onConnectionProgress: (s) => setLastConnectionProgressEvent(s), }); - const { paddingBottom, onLayout } = useBottomTabPadding(12); + const marginBottom = useBottomTabSpacing(); const connectionForm = useAppForm({ // https://tanstack.com/form/latest/docs/framework/react/guides/async-initial-values defaultValues, @@ -86,10 +86,9 @@ function Host() { return ( { - const extra = windowH - (y + height); - return extra > 0 ? extra : 0; - }; - - // Measure any bottom overlap (e.g., native tab bar) and add padding to avoid it - const [bottomExtra, setBottomExtra] = useState(0); + const marginBottom = useBottomTabSpacing(); return ( - { - const { y, height } = e.nativeEvent.layout; - const extra = computeBottomExtra(y, height); - if (extra !== bottomExtra) setBottomExtra(extra); - }} - style={{ - flex: 1, - justifyContent: 'flex-start', - backgroundColor: theme.colors.background, - paddingTop: 2, - paddingLeft: 8, - paddingRight: 8, - paddingBottom: insets.bottom + (bottomExtra || estimatedTabBarHeight), - }} - > - ( - { - if (!connection) return; - try { - await connection.disconnect(); - } catch (e) { - console.warn('Failed to disconnect', e); - } - }} - > - - - ), + <> + - { - if (terminalReadyRef.current) return; - terminalReadyRef.current = true; - - if (!shell) throw new Error('Shell not found'); - - // Replay from head, then attach live listener - void (async () => { - const res = shell.readBuffer({ mode: 'head' }); - console.log('readBuffer(head)', { - chunks: res.chunks.length, - nextSeq: res.nextSeq, - dropped: res.dropped, - }); - if (res.chunks.length) { - const chunks = res.chunks.map((c) => c.bytes); - const xr = xtermRef.current; - if (xr) { - xr.writeMany(chunks.map((c) => new Uint8Array(c))); - xr.flush(); - } - } - const id = shell.addListener( - (ev: ListenerEvent) => { - if ('kind' in ev) { - console.log('listener.dropped', ev); - return; - } - const chunk = ev; - const xr3 = xtermRef.current; - if (xr3) xr3.write(new Uint8Array(chunk.bytes)); + > + ( + { + if (!connection) return; + try { + await connection.disconnect(); + } catch (e) { + console.warn('Failed to disconnect', e); + } + }} + > + + + ), + }} + /> + + { - if (!shell) return; - const bytes = encoder.encode(terminalMessage); - shell.sendData(bytes.buffer).catch((e: unknown) => { - console.warn('sendData failed', e); - router.back(); - }); + }} + onInitialized={() => { + if (terminalReadyRef.current) return; + terminalReadyRef.current = true; + + if (!shell) throw new Error('Shell not found'); + + // Replay from head, then attach live listener + void (async () => { + const res = shell.readBuffer({ mode: 'head' }); + console.log('readBuffer(head)', { + chunks: res.chunks.length, + nextSeq: res.nextSeq, + dropped: res.dropped, + }); + if (res.chunks.length) { + const chunks = res.chunks.map((c) => c.bytes); + const xr = xtermRef.current; + if (xr) { + xr.writeMany(chunks.map((c) => new Uint8Array(c))); + xr.flush(); + } + } + const id = shell.addListener( + (ev: ListenerEvent) => { + if ('kind' in ev) { + console.log('listener.dropped', ev); + return; + } + const chunk = ev; + const xr3 = xtermRef.current; + if (xr3) xr3.write(new Uint8Array(chunk.bytes)); + }, + { cursor: { mode: 'seq', seq: res.nextSeq } }, + ); + console.log('shell listener attached', id.toString()); + listenerIdRef.current = id; + })(); + // Focus to pop the keyboard (iOS needs the prop we set) + const xr2 = xtermRef.current; + if (xr2) xr2.focus(); + }} + onData={(terminalMessage) => { + if (!shell) return; + const bytes = encoder.encode(terminalMessage); + shell.sendData(bytes.buffer).catch((e: unknown) => { + console.warn('sendData failed', e); + router.back(); + }); + }} + /> + + + - + ); } diff --git a/apps/mobile/src/app/(test)/_layout.tsx b/apps/mobile/src/app/(test)/_layout.tsx index 9fb5533..46fcf24 100644 --- a/apps/mobile/src/app/(test)/_layout.tsx +++ b/apps/mobile/src/app/(test)/_layout.tsx @@ -9,6 +9,7 @@ import { useTheme } from '@/lib/theme'; export default function TabsLayout() { const theme = useTheme(); + return ( - + ); } @@ -62,15 +66,21 @@ const TextInputAndLabel = (props: CustomTextInputProps) => { const { title, ...rest } = props; const [isFocused, setFocused] = useState(false); - const onFocus = useCallback>((e) => { - setFocused(true); - props.onFocus?.(e); - }, []); + const onFocus = useCallback>( + (e) => { + setFocused(true); + props.onFocus?.(e); + }, + [props], + ); - const onBlur = useCallback>((e) => { - setFocused(false); - props.onBlur?.(e); - }, []); + const onBlur = useCallback>( + (e) => { + setFocused(false); + props.onBlur?.(e); + }, + [props], + ); return ( <> diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index 7d618d5..d197633 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -1,5 +1,5 @@ import { Redirect } from 'expo-router'; export default function RootRedirect() { - return ; + return ; } diff --git a/apps/mobile/src/lib/useBottomTabPadding.ts b/apps/mobile/src/lib/useBottomTabPadding.ts deleted file mode 100644 index 06ef485..0000000 --- a/apps/mobile/src/lib/useBottomTabPadding.ts +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import { Dimensions, Platform } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; - -type LayoutEvent = { - nativeEvent: { - layout: { - y: number; - height: number; - }; - }; -}; - -export function useBottomTabPadding(basePadding = 12) { - const insets = useSafeAreaInsets(); - const windowH = Dimensions.get('window').height; - const estimatedTabBarHeight = Platform.select({ - ios: 49, - android: 80, - default: 56, - }); - const [bottomExtra, setBottomExtra] = React.useState(0); - - const onLayout = React.useCallback( - (e: LayoutEvent) => { - const { y, height } = e.nativeEvent.layout; - const extra = windowH - (y + height); - setBottomExtra(extra > 0 ? extra : 0); - }, - [windowH], - ); - - const paddingBottom = - basePadding + insets.bottom + (bottomExtra || estimatedTabBarHeight!); - return { paddingBottom, onLayout } as const; -} diff --git a/apps/mobile/src/lib/useBottomTabSpacing.ts b/apps/mobile/src/lib/useBottomTabSpacing.ts new file mode 100644 index 0000000..e00c341 --- /dev/null +++ b/apps/mobile/src/lib/useBottomTabSpacing.ts @@ -0,0 +1,12 @@ +import { Platform } from 'react-native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +export function useBottomTabSpacing() { + const insets = useSafeAreaInsets(); + const estimatedTabBarHeight = Platform.select({ + ios: 49, + android: 80, + default: 56, + }); + return insets.bottom + estimatedTabBarHeight; +} diff --git a/packages/react-native-xtermjs-webview/src-internal/main.tsx b/packages/react-native-xtermjs-webview/src-internal/main.tsx index 27092e9..0649b86 100644 --- a/packages/react-native-xtermjs-webview/src-internal/main.tsx +++ b/packages/react-native-xtermjs-webview/src-internal/main.tsx @@ -153,7 +153,7 @@ window.onload = () => { window.addEventListener('message', handler); // Initial handshake (send once) - setTimeout(() => sendToRn({ type: 'initialized' }), 50); + setTimeout(() => sendToRn({ type: 'initialized' }), 100); } catch (e) { sendToRn({ type: 'debug',