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.
This commit is contained in:
Mohamed Boudra
2026-07-21 23:53:33 +02:00
parent 97cfcf8306
commit 21d9bdc644
4 changed files with 43 additions and 24 deletions

View File

@@ -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],

View File

@@ -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,

View File

@@ -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,

View File

@@ -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],