From 21d9bdc644d9e4b0c4c337e6bac5c69c92e90dc9 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 21 Jul 2026 23:53:33 +0200 Subject: [PATCH] fix(app): stop composer flickering during keyboard changes Keep layout-only styles on stable React Native style IDs when they share Reanimated hosts with keyboard transforms. This prevents style regeneration from disturbing the animated native views while preserving keyboard and safe-area behavior. --- .../app/src/composer/draft/workspace-tab.tsx | 18 ++++++++----- packages/app/src/composer/index.tsx | 8 ++++-- packages/app/src/panels/agent-panel.tsx | 26 ++++++++++++------- .../app/src/screens/new-workspace-screen.tsx | 15 ++++++----- 4 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/app/src/composer/draft/workspace-tab.tsx b/packages/app/src/composer/draft/workspace-tab.tsx index 3ac357fb8..7ac294915 100644 --- a/packages/app/src/composer/draft/workspace-tab.tsx +++ b/packages/app/src/composer/draft/workspace-tab.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useRef } from "react"; -import { Keyboard, ScrollView, Text, View } from "react-native"; +import { Keyboard, ScrollView, StyleSheet as RNStyleSheet, Text, View } from "react-native"; import { useTranslation } from "react-i18next"; import ReanimatedAnimated from "react-native-reanimated"; import { StyleSheet } from "react-native-unistyles"; @@ -665,7 +665,11 @@ export function WorkspaceDraftAgentTab({ }); const inputAreaWrapperStyle = useMemo( - () => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }, composerKeyboardStyle], + () => [ + animatedStaticStyles.inputAreaWrapper, + { paddingBottom: insets.bottom }, + composerKeyboardStyle, + ], [insets.bottom, composerKeyboardStyle], ); @@ -772,6 +776,12 @@ export function WorkspaceDraftAgentTab({ ); } +const animatedStaticStyles = RNStyleSheet.create({ + inputAreaWrapper: { + width: "100%", + }, +}); + const styles = StyleSheet.create((theme) => ({ container: { flex: 1, @@ -795,10 +805,6 @@ const styles = StyleSheet.create((theme) => ({ configSection: { gap: theme.spacing[3], }, - inputAreaWrapper: { - width: "100%", - backgroundColor: theme.colors.surface0, - }, importPillRow: { width: "100%", paddingHorizontal: theme.spacing[4], diff --git a/packages/app/src/composer/index.tsx b/packages/app/src/composer/index.tsx index 216342744..bd7787760 100644 --- a/packages/app/src/composer/index.tsx +++ b/packages/app/src/composer/index.tsx @@ -3,6 +3,7 @@ import { Pressable, Text, ActivityIndicator, + StyleSheet as RNStyleSheet, type PressableStateCallbackType, } from "react-native"; import type { TFunction } from "i18next"; @@ -1916,7 +1917,7 @@ export function Composer({ ); const composerContainerStyle = useMemo( - () => [styles.container, keyboardAnimatedStyle], + () => [animatedStaticStyles.container, keyboardAnimatedStyle], [keyboardAnimatedStyle], ); const inputAreaContainerStyle = useMemo( @@ -2077,11 +2078,14 @@ export function Composer({ ); } -const styles = StyleSheet.create((theme: Theme) => ({ +const animatedStaticStyles = RNStyleSheet.create({ container: { flexDirection: "column", position: "relative", }, +}); + +const styles = StyleSheet.create((theme: Theme) => ({ borderSeparator: { height: theme.borderWidth[1], backgroundColor: theme.colors.border, diff --git a/packages/app/src/panels/agent-panel.tsx b/packages/app/src/panels/agent-panel.tsx index edde7a42b..e2d915ecc 100644 --- a/packages/app/src/panels/agent-panel.tsx +++ b/packages/app/src/panels/agent-panel.tsx @@ -11,7 +11,7 @@ import React, { useSyncExternalStore, } from "react"; import { useTranslation } from "react-i18next"; -import { ActivityIndicator, Text, View } from "react-native"; +import { ActivityIndicator, StyleSheet as RNStyleSheet, Text, View } from "react-native"; import ReanimatedAnimated from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, withUnistyles } from "react-native-unistyles"; @@ -1024,7 +1024,7 @@ function ChatAgentContent({ ]); const animatedContentStyle = useMemo( - () => [styles.content, animatedKeyboardStyle], + () => [animatedStaticStyles.content, animatedKeyboardStyle], [animatedKeyboardStyle], ); @@ -1468,7 +1468,11 @@ function ActiveAgentComposer({ }); const inputAreaStyle = useMemo( - () => [styles.inputAreaWrapper, { paddingBottom: insets.bottom }, composerKeyboardStyle], + () => [ + animatedStaticStyles.inputAreaWrapper, + { paddingBottom: insets.bottom }, + composerKeyboardStyle, + ], [insets.bottom, composerKeyboardStyle], ); @@ -1593,6 +1597,15 @@ const foregroundColorMapping = (theme: Theme) => ({ color: theme.colors.foreground, }); +const animatedStaticStyles = RNStyleSheet.create({ + content: { + flex: 1, + }, + inputAreaWrapper: { + width: "100%", + }, +}); + const styles = StyleSheet.create((theme) => ({ root: { flex: 1, @@ -1607,13 +1620,6 @@ const styles = StyleSheet.create((theme) => ({ overflow: "hidden", ...(isWeb ? { userSelect: "none" as const } : {}), }, - content: { - flex: 1, - }, - inputAreaWrapper: { - width: "100%", - backgroundColor: theme.colors.surface0, - }, historySyncOverlay: { position: "absolute", top: 0, diff --git a/packages/app/src/screens/new-workspace-screen.tsx b/packages/app/src/screens/new-workspace-screen.tsx index a2a2fea7d..e60adaf2e 100644 --- a/packages/app/src/screens/new-workspace-screen.tsx +++ b/packages/app/src/screens/new-workspace-screen.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "r import type { ReactElement, RefObject } from "react"; import { useTranslation } from "react-i18next"; import type { TFunction } from "i18next"; -import { Pressable, Text, View } from "react-native"; +import { Pressable, StyleSheet as RNStyleSheet, Text, View } from "react-native"; import type { PressableStateCallbackType } from "react-native"; import ReanimatedAnimated from "react-native-reanimated"; import { StyleSheet, useUnistyles, withUnistyles } from "react-native-unistyles"; @@ -2034,7 +2034,7 @@ export function NewWorkspaceScreen({ }); const centeredStyle = useMemo( - () => [styles.centered, composerKeyboardStyle], + () => [animatedStaticStyles.centered, composerKeyboardStyle], [composerKeyboardStyle], ); @@ -2163,6 +2163,13 @@ export function NewWorkspaceScreen({ ); } +const animatedStaticStyles = RNStyleSheet.create({ + centered: { + width: "100%", + maxWidth: MAX_CONTENT_WIDTH, + }, +}); + const styles = StyleSheet.create((theme) => ({ container: { flex: 1, @@ -2181,10 +2188,6 @@ const styles = StyleSheet.create((theme) => ({ contentCompact: { justifyContent: "flex-end", }, - centered: { - width: "100%", - maxWidth: MAX_CONTENT_WIDTH, - }, composerTitleContainer: { marginBottom: theme.spacing[8], paddingLeft: theme.spacing[6],