diff --git a/apps/marketing/src/lib/site.ts b/apps/marketing/src/lib/site.ts index 4d0c6da86c43..dff2241f50c1 100644 --- a/apps/marketing/src/lib/site.ts +++ b/apps/marketing/src/lib/site.ts @@ -7,6 +7,6 @@ export const ANDROID_PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=com.t3tools.t3code"; export const MARKETING_STATS = { - githubStars: "21k+", - users: "200,000", + githubStars: "22k+", + users: "300,000", } as const; diff --git a/apps/marketing/vercel.ts b/apps/marketing/vercel.ts index fe11ddd4c069..e37be215f1a0 100644 --- a/apps/marketing/vercel.ts +++ b/apps/marketing/vercel.ts @@ -7,4 +7,11 @@ export const config: VercelConfig = { installCommand: "npm install -g vite-plus && vp install --filter '@t3tools/marketing...'", buildCommand: "vp run --filter @t3tools/marketing build", outputDirectory: "dist", + redirects: [ + { + source: "/app", + destination: "https://app.t3.codes", + permanent: true, + }, + ], }; diff --git a/apps/mobile/modules/t3-markdown-text/android/src/main/java/expo/modules/t3markdowntext/T3MarkdownTextSelectionModule.kt b/apps/mobile/modules/t3-markdown-text/android/src/main/java/expo/modules/t3markdowntext/T3MarkdownTextSelectionModule.kt index af8675831f2d..8e9fa3894aa9 100644 --- a/apps/mobile/modules/t3-markdown-text/android/src/main/java/expo/modules/t3markdowntext/T3MarkdownTextSelectionModule.kt +++ b/apps/mobile/modules/t3-markdown-text/android/src/main/java/expo/modules/t3markdowntext/T3MarkdownTextSelectionModule.kt @@ -3,6 +3,8 @@ package expo.modules.t3markdowntext import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.text.Spannable +import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.ReplacementSpan import android.view.ActionMode @@ -18,6 +20,14 @@ import kotlin.math.min private const val OBJECT_REPLACEMENT_CHARACTER = "\uFFFC" +// Match React Native's measurement buffer. Android orders tied line-height +// spans differently in SpannableString, shifting inline images once RN's +// span priorities are exhausted. +private object MarkdownSpannableFactory : Spannable.Factory() { + override fun newSpannable(source: CharSequence): Spannable = + SpannableStringBuilder(source) +} + private fun copyTextWithoutInlineImages( text: CharSequence, start: Int, @@ -85,6 +95,7 @@ class T3MarkdownTextSelectionModule : Module() { if (currentCallback is SanitizingSelectionActionModeCallback) { return@runOnUiQueueThread } + textView.setSpannableFactory(MarkdownSpannableFactory) textView.customSelectionActionModeCallback = SanitizingSelectionActionModeCallback(textView, currentCallback) } diff --git a/apps/mobile/modules/t3-markdown-text/package.json b/apps/mobile/modules/t3-markdown-text/package.json index 8922c8868c44..376befbeb152 100644 --- a/apps/mobile/modules/t3-markdown-text/package.json +++ b/apps/mobile/modules/t3-markdown-text/package.json @@ -25,7 +25,7 @@ "./links": "./src/markdownLinks.ts", "./markdown": "./src/nativeMarkdownText.ts", "./primitive": "./src/MarkdownTextPrimitive.tsx", - "./renderer": "./src/SelectableMarkdownText.ios.tsx", + "./renderer": "./src/SelectableMarkdownText.tsx", "./types": "./src/SelectableMarkdownText.types.ts" }, "peerDependencies": { diff --git a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.tsx similarity index 99% rename from apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx rename to apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.tsx index 348a3c489a2c..ab75eaf32a2c 100644 --- a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx +++ b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.tsx @@ -5,7 +5,7 @@ import type { MarkdownNode } from "react-native-nitro-markdown/headless"; import { CopyTextButton } from "./CopyTextButton"; import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive"; import { nativeMarkdownDocumentRuns, nativeMarkdownListItemBlocks } from "./nativeMarkdownText"; -import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios"; +import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText"; import type { MarkdownCodeHighlighter, MarkdownHighlightedToken, diff --git a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.tsx similarity index 100% rename from apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx rename to apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.tsx diff --git a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.ios.tsx b/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.ios.tsx deleted file mode 100644 index 2a231c603584..000000000000 --- a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.ios.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useMemo } from "react"; -import { View } from "react-native"; -import { parseMarkdownWithOptions } from "react-native-nitro-markdown/headless"; - -import { - nativeMarkdownChunkSpacing, - nativeMarkdownDocumentChunks, - nativeMarkdownDocumentRuns, - nativeMarkdownWithPreservedSoftBreaks, -} from "./nativeMarkdownText"; -import { MarkdownImageRendererContext, NativeMarkdownBlock } from "./NativeMarkdownBlock.ios"; -import { - MarkdownFileContextMenuContext, - NativeMarkdownSelectableText, - type MarkdownFileContextMenuHandlers, -} from "./NativeMarkdownSelectableText.ios"; -import type { - SelectableMarkdownSkill, - SelectableMarkdownTextProps, -} from "./SelectableMarkdownText.types"; - -const EMPTY_SKILLS: ReadonlyArray = []; - -export type { - MarkdownCodeHighlighter, - MarkdownHighlightedToken, - MarkdownImageRenderer, - MarkdownImageRequest, - NativeMarkdownTextStyle, - SelectableMarkdownSkill, - SelectableMarkdownTextProps, -} from "./SelectableMarkdownText.types"; - -export function hasNativeSelectableMarkdownText(): boolean { - return true; -} - -export function SelectableMarkdownText({ - markdown, - skills = EMPTY_SKILLS, - textStyle, - highlightCode, - preserveSoftBreaks = false, - onLinkPress, - fileContextMenu, - onFileContextMenuAction, - renderImage, - marginTop = 0, - marginBottom = 0, -}: SelectableMarkdownTextProps) { - const chunks = useMemo(() => { - const parsedDocument = parseMarkdownWithOptions(markdown, { - gfm: true, - html: true, - math: false, - }); - const document = preserveSoftBreaks - ? nativeMarkdownWithPreservedSoftBreaks(parsedDocument) - : parsedDocument; - return nativeMarkdownDocumentChunks(document).map((chunk) => - chunk.kind === "selectable" - ? { - ...chunk, - runs: nativeMarkdownDocumentRuns(chunk.node, skills), - } - : chunk, - ); - }, [markdown, preserveSoftBreaks, skills]); - - const fileContextMenuHandlers = useMemo( - () => - fileContextMenu && onFileContextMenuAction - ? { fileContextMenu, onFileContextMenuAction } - : null, - [fileContextMenu, onFileContextMenuAction], - ); - - return ( - - - {/* A percentage width here creates a cyclic intrinsic measurement inside - shrink-to-fit containers such as user-message bubbles. Yoga then gives - the native text node an unbounded second pass and the parent only clips - the resulting single-line width instead of reflowing it. */} - - {chunks.map((chunk, index) => { - const content = - chunk.kind === "rich" ? ( - - ) : ( - - ); - - return ( - - {content} - - ); - })} - - - - ); -} diff --git a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx b/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx index 006d33e7259d..188a93b30e12 100644 --- a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx +++ b/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.tsx @@ -1,4 +1,25 @@ -import type { SelectableMarkdownTextProps } from "./SelectableMarkdownText.types"; +import { useMemo } from "react"; +import { View } from "react-native"; +import { parseMarkdownWithOptions } from "react-native-nitro-markdown/headless"; + +import { + nativeMarkdownChunkSpacing, + nativeMarkdownDocumentChunks, + nativeMarkdownDocumentRuns, + nativeMarkdownWithPreservedSoftBreaks, +} from "./nativeMarkdownText"; +import { MarkdownImageRendererContext, NativeMarkdownBlock } from "./NativeMarkdownBlock"; +import { + MarkdownFileContextMenuContext, + NativeMarkdownSelectableText, + type MarkdownFileContextMenuHandlers, +} from "./NativeMarkdownSelectableText"; +import type { + SelectableMarkdownSkill, + SelectableMarkdownTextProps, +} from "./SelectableMarkdownText.types"; + +const EMPTY_SKILLS: ReadonlyArray = []; export type { MarkdownCodeHighlighter, @@ -10,6 +31,87 @@ export type { SelectableMarkdownTextProps, } from "./SelectableMarkdownText.types"; -export function SelectableMarkdownText(_props: SelectableMarkdownTextProps) { - return null; +export function hasNativeSelectableMarkdownText(): boolean { + return true; +} + +export function SelectableMarkdownText({ + markdown, + skills = EMPTY_SKILLS, + textStyle, + highlightCode, + preserveSoftBreaks = false, + onLinkPress, + fileContextMenu, + onFileContextMenuAction, + renderImage, + marginTop = 0, + marginBottom = 0, +}: SelectableMarkdownTextProps) { + const chunks = useMemo(() => { + const parsedDocument = parseMarkdownWithOptions(markdown, { + gfm: true, + html: true, + math: false, + }); + const document = preserveSoftBreaks + ? nativeMarkdownWithPreservedSoftBreaks(parsedDocument) + : parsedDocument; + return nativeMarkdownDocumentChunks(document).map((chunk) => + chunk.kind === "selectable" + ? { + ...chunk, + runs: nativeMarkdownDocumentRuns(chunk.node, skills), + } + : chunk, + ); + }, [markdown, preserveSoftBreaks, skills]); + + const fileContextMenuHandlers = useMemo( + () => + fileContextMenu && onFileContextMenuAction + ? { fileContextMenu, onFileContextMenuAction } + : null, + [fileContextMenu, onFileContextMenuAction], + ); + + return ( + + + {/* A percentage width here creates a cyclic intrinsic measurement inside + shrink-to-fit containers such as user-message bubbles. Yoga then gives + the native text node an unbounded second pass and the parent only clips + the resulting single-line width instead of reflowing it. */} + + {chunks.map((chunk, index) => { + const content = + chunk.kind === "rich" ? ( + + ) : ( + + ); + + return ( + + {content} + + ); + })} + + + + ); } diff --git a/apps/mobile/package.json b/apps/mobile/package.json index d5285f263fd7..a5d2f40b4a93 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -147,6 +147,7 @@ "buildFromSource": [ "expo-notifications", "react-native-screens", + "react-native-reanimated", "@react-native-menu/menu", "expo-audio" ] diff --git a/apps/mobile/src/components/ComposerAttachmentButton.tsx b/apps/mobile/src/components/ComposerAttachmentButton.tsx index 1af72d8883d7..8ea70a45c020 100644 --- a/apps/mobile/src/components/ComposerAttachmentButton.tsx +++ b/apps/mobile/src/components/ComposerAttachmentButton.tsx @@ -40,6 +40,9 @@ export function ComposerAttachmentButton(props: { return ( { if (nativeEvent.event === "photos") { diff --git a/apps/mobile/src/components/MediaVideoPlayer.tsx b/apps/mobile/src/components/MediaVideoPlayer.tsx index 0e3d8416affa..d06b8bcca04c 100644 --- a/apps/mobile/src/components/MediaVideoPlayer.tsx +++ b/apps/mobile/src/components/MediaVideoPlayer.tsx @@ -19,6 +19,7 @@ function LoadedMediaVideo(props: { }) { const focused = useIsFocused(); const active = useRef(focused && AppState.currentState === "active"); + const fullscreen = useRef(false); const [attempt, setAttempt] = useState(0); // Expo's Android player also reports completed playback as idle. const [loadState, setLoadState] = useState<"pending" | "complete" | "error">("pending"); @@ -38,10 +39,12 @@ function LoadedMediaVideo(props: { useEffect(() => { active.current = focused && !props.paused && AppState.currentState === "active"; - if (!active.current) player.pause(); + if (!focused || props.paused || (!active.current && !fullscreen.current)) player.pause(); + // Native background handling distinguishes Android's fullscreen activity + // from leaving the app; React Native reports both as background. const subscription = AppState.addEventListener("change", (state) => { active.current = focused && !props.paused && state === "active"; - if (!active.current) player.pause(); + if (state === "inactive" || (state === "background" && !fullscreen.current)) player.pause(); }); return () => subscription.remove(); }, [focused, player, props.paused]); @@ -70,6 +73,12 @@ function LoadedMediaVideo(props: { nativeControls contentFit="contain" fullscreenOptions={{ enable: true }} + onFullscreenEnter={() => { + fullscreen.current = true; + }} + onFullscreenExit={() => { + fullscreen.current = false; + }} allowsPictureInPicture={false} /> {loadState === "error" || (loadState === "complete" && status === "error") ? ( diff --git a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx index 33b99dd8e8ce..50c92ed1f590 100644 --- a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx +++ b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx @@ -18,6 +18,7 @@ import { useEnvironmentQuery } from "../../state/query"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { FileTreeBrowser } from "./FileTreeBrowser"; import { preloadWorkspaceFileContents } from "./preload-workspace-file"; +import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; export function ThreadFileNavigatorPane(props: { readonly cwd: string; @@ -28,6 +29,7 @@ export function ThreadFileNavigatorPane(props: { readonly onSelectFile: (path: string) => void; }) { const [searchQuery, setSearchQuery] = useState(""); + const { toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); const { themeAppearance: highlightTheme } = useAppearancePreferences(); const theme = useUniwindTheme(); const foregroundColor = theme["--color-foreground"]; @@ -64,8 +66,18 @@ export function ThreadFileNavigatorPane(props: { type: "button" as const, width: 44, }, + { + accessibilityLabel: "Close files", + icon: { name: "xmark", type: "sfSymbol" as const }, + identifier: "thread-file-navigator-close", + onPress: toggleAuxiliaryPane, + sharesBackground: false, + tintColor: foregroundColor, + type: "button" as const, + width: 44, + }, ] as ComponentProps["headerRightBarButtonItems"], - [entriesQuery.refresh, foregroundColor], + [entriesQuery.refresh, foregroundColor, toggleAuxiliaryPane], ); const fileTree = ( @@ -159,6 +171,15 @@ export function ThreadFileNavigatorPane(props: { type="monochrome" /> + + + + Math.max(0, width - renderedSidebarWidth.value - renderedInspectorWidth.value), + ); const handleSelectThread = useCallback( (thread: EnvironmentThreadShell) => { @@ -583,10 +593,15 @@ function AdaptiveWorkspaceLayoutContent( contentSettledWidth !== null ? { flex: 1, width: contentSettledWidth } : { flex: 1 } } > - {props.children} + + {props.children} + | null>(null); + +export function useWorkspaceContentWidth() { + return use(WorkspaceContentWidthContext); +} diff --git a/apps/mobile/src/features/layout/workspace-inspector-pane.tsx b/apps/mobile/src/features/layout/workspace-inspector-pane.tsx index 9825ed7fdf5b..7c6574e22e6f 100644 --- a/apps/mobile/src/features/layout/workspace-inspector-pane.tsx +++ b/apps/mobile/src/features/layout/workspace-inspector-pane.tsx @@ -4,6 +4,7 @@ import Animated, { useAnimatedStyle, useSharedValue, withTiming, + type SharedValue, } from "react-native-reanimated"; import { constrainAuxiliaryPaneWidth, type WorkspacePaneLayout } from "../../lib/layout"; @@ -22,6 +23,7 @@ import { WorkspacePaneDivider } from "./workspace-pane-divider"; * module stays import-cycle-free with AdaptiveWorkspaceLayout. */ export function WorkspaceInspectorPane(props: { + readonly renderedInspectorWidth: SharedValue; /** * When false the pane animates closed but keeps its content mounted for the * exit transition (a route that lost focus). `onClosed` fires once the @@ -45,7 +47,7 @@ export function WorkspaceInspectorPane(props: { // inspector at its final position so route replacement never replays an // entering transition. Only visibility and explicit resizing change it. const inspectorProgress = useSharedValue(inspectorVisible ? 1 : 0); - const renderedInspectorWidth = useSharedValue(inspectorVisible ? (inspectorWidth ?? 0) : 0); + const { renderedInspectorWidth } = props; // The content keeps its own width so the reveal (outer width) clips a // fully-laid-out pane instead of reflowing text every frame. When the OPEN // pane's target width changes (e.g. the sidebar toggles and reserves diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index bc2e6fc3c64a..6829a88cef74 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -850,6 +850,19 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) [navigation, selectedThread, terminalId], ); + const handleCloseTerminal = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack(); + return; + } + navigation.dispatch( + StackActions.replace("Thread", { + environmentId: params.environmentId, + threadId: params.threadId, + }), + ); + }, [navigation, params.environmentId, params.threadId]); + const navigateAwayAfterExit = useCallback(() => { // With other shells still live, fall through to the previous one instead // of dropping the user back on the thread. @@ -1143,7 +1156,7 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) navigation.goBack() : undefined} + onBack={handleCloseTerminal} trailing={ <> {layout.usesSplitView ? ( @@ -1179,6 +1192,12 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) {layout.usesSplitView ? ( + { + "worklet"; + const timing = { + duration: COMPOSER_TRANSITION_DURATION_MS, + reduceMotion: ReduceMotion.System, + }; + return { + initialValues: { + originX: values.targetOriginX, + originY: values.currentOriginY, + width: values.targetWidth, + height: values.currentHeight, + }, + animations: { + originX: values.targetOriginX, + originY: withTiming(values.targetOriginY, timing), + width: values.targetWidth, + height: withTiming(values.targetHeight, timing), + }, + }; +}; export const COMPOSER_LAYOUT_TRANSITION = - Platform.OS === "android" - ? undefined - : LinearTransition.duration(COMPOSER_TRANSITION_DURATION_MS).reduceMotion(ReduceMotion.System); + Platform.OS === "android" ? undefined : composerHeightTransition; const COMPOSER_ATTACHMENT_ENTERING = Platform.OS === "android" @@ -351,7 +373,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer ); const isVoiceInputPresented = voicePresentation.statusLabel !== null; // An open draft stays visible; only a collapsed composer becomes a voice strip. - const isExpanded = isFocused || settingsSheetPresentation.isActive; + const isExpanded = isFocused || settingsSheetPresentation.keepsComposerExpanded; const showsCompactDictation = isVoiceInputPresented && !isExpanded; const isToolbarVisible = isExpanded || isVoiceInputPresented; const attachmentBlockReason = composerAttachmentUploadBlockReason({ @@ -407,11 +429,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const handleBlur = useCallback(() => { setIsFocused(false); - if (!settingsSheetPresentation.isActive) { + if (!settingsSheetPresentation.keepsComposerExpanded) { onExpandedChange?.(false); } onEditorFocusChange?.(false); - }, [onEditorFocusChange, onExpandedChange, settingsSheetPresentation.isActive]); + }, [onEditorFocusChange, onExpandedChange, settingsSheetPresentation.keepsComposerExpanded]); const handleSend = useCallback(async () => { // Typed out in full rather than picked from the menu. Attachments mean the // user is sending a prompt, so those go through as usual. diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 42bb4df1495a..55cd9e9a6c83 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -58,10 +58,12 @@ import Animated, { FadeOut, ReduceMotion, useAnimatedReaction, + useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { useWorkspaceContentWidth } from "../layout/workspace-content-width"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { collectProviderUsageLimits } from "@t3tools/shared/usageLimits"; @@ -647,6 +649,12 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const layoutVariant = props.layoutVariant ?? "compact"; const isSplitLayout = layoutVariant === "split"; const contentMaxWidth = isSplitLayout ? CHAT_CONTENT_MAX_WIDTH : undefined; + const workspaceContentWidth = useWorkspaceContentWidth(); + const composerWidthStyle = useAnimatedStyle(() => + isSplitLayout && workspaceContentWidth !== null + ? { width: workspaceContentWidth.value, right: undefined } + : { width: undefined, right: 0 }, + ); const selectedInstanceId = props.selectedThread.modelSelection.instanceId; useStreamingHaptics(props.selectedThread.id, props.selectedThreadFeed); const selectedProviderSkills = useMemo(() => { @@ -915,7 +923,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread {/* No paddingTop here: the overlay's measured height becomes the list's bottom inset, so any padding above the pill/composer diff --git a/apps/mobile/src/features/threads/use-thread-settings-sheet-presentation.ts b/apps/mobile/src/features/threads/use-thread-settings-sheet-presentation.ts index b5b4914ad11e..805b9acf95dc 100644 --- a/apps/mobile/src/features/threads/use-thread-settings-sheet-presentation.ts +++ b/apps/mobile/src/features/threads/use-thread-settings-sheet-presentation.ts @@ -3,7 +3,7 @@ import { KeyboardController } from "react-native-keyboard-controller"; import type { ComposerEditorHandle } from "../../components/ComposerEditor"; -type PresentationPhase = "closed" | "opening" | "visible"; +type PresentationPhase = "closed" | "opening" | "visible" | "restoring"; /** * The navigator-level UIKit completion event added by the repo's @@ -91,7 +91,8 @@ export function useThreadSettingsSheetPresentation(input: { focusRestoreIdRef.current += 1; clearDismissRestoreTimer(); restorePendingRef.current = false; - restoreFocusAfterDismissRef.current = input.isEditorFocused || KeyboardController.isVisible(); + restoreFocusAfterDismissRef.current = + phase === "restoring" || input.isEditorFocused || KeyboardController.isVisible(); setPhase("opening"); const openingId = openingIdRef.current + 1; @@ -109,7 +110,7 @@ export function useThreadSettingsSheetPresentation(input: { } setPhase("visible"); }); - }, [clearDismissRestoreTimer, input.editorRef, input.isEditorFocused]); + }, [clearDismissRestoreTimer, input.editorRef, input.isEditorFocused, phase]); const restoreEditorFocus = useCallback(() => { const focusRestoreId = focusRestoreIdRef.current + 1; @@ -120,12 +121,11 @@ export function useThreadSettingsSheetPresentation(input: { // normally succeeds; the retries are insurance against UIKit briefly // refusing first-responder status right at the transition boundary. const restoreFocus = () => { - if ( - !isMountedRef.current || - focusRestoreIdRef.current !== focusRestoreId || - isEditorFocusedRef.current || - attemptsRemaining <= 0 - ) { + if (!isMountedRef.current || focusRestoreIdRef.current !== focusRestoreId) { + return; + } + if (isEditorFocusedRef.current || attemptsRemaining <= 0) { + setPhase("closed"); return; } @@ -157,11 +157,15 @@ export function useThreadSettingsSheetPresentation(input: { */ const onDismissed = useCallback(() => { isActiveRef.current = false; - setPhase("closed"); if (!restoreFocusAfterDismissRef.current) { + setPhase("closed"); return; } + // Keep the card expanded across the handoff back to its editor. With a + // hardware keyboard there is no software-keyboard travel to hide a collapse + // while the sheet dismissal and focus restoration finish. + setPhase("restoring"); restoreFocusAfterDismissRef.current = false; restorePendingRef.current = true; clearDismissRestoreTimer(); @@ -185,7 +189,8 @@ export function useThreadSettingsSheetPresentation(input: { }, [runPendingDismissalRestore]); return { - isActive: phase !== "closed", + isActive: phase === "opening" || phase === "visible", + keepsComposerExpanded: phase !== "closed", isVisible: phase === "visible", open, onDismissed, diff --git a/apps/web/src/components/CommandPalette.logic.test.ts b/apps/web/src/components/CommandPalette.logic.test.ts index b82147b376c8..25896baf7555 100644 --- a/apps/web/src/components/CommandPalette.logic.test.ts +++ b/apps/web/src/components/CommandPalette.logic.test.ts @@ -3,6 +3,7 @@ import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools import type { Project, Thread } from "../types"; import { buildBrowseGroups, + buildCommandPaletteProjectMetadata, buildProjectActionItems, buildThreadActionItems, buildLinkedThreadActionItems, @@ -55,6 +56,124 @@ describe("linked pull request thread navigation", () => { }); }); +describe("buildCommandPaletteProjectMetadata", () => { + const localEnvironmentId = EnvironmentId.make("environment-local"); + const remoteEnvironmentId = EnvironmentId.make("environment-build-box"); + const locations = new Map([ + [localEnvironmentId, { kind: "local" as const, label: "Local", machine: "laptop" as const }], + [ + remoteEnvironmentId, + { kind: "remote" as const, label: "Build box", machine: "server" as const }, + ], + ]); + + it("makes every member environment and path searchable", () => { + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: localEnvironmentId, + title: "T3 Code", + workspaceRoot: "/Users/theo/Projects/t3code", + }, + { + environmentId: remoteEnvironmentId, + title: "t3code", + workspaceRoot: "/srv/t3code", + }, + ], + locationByEnvironmentId: locations, + }); + + expect(metadata.searchTerms).toEqual([ + "T3 Code", + "/Users/theo/Projects/t3code", + "Local", + "t3code", + "/srv/t3code", + "Build box", + ]); + expect(metadata.environmentLabels).toEqual(["Local", "Build box"]); + + const [filteredGroup] = filterCommandPaletteGroups({ + activeGroups: [], + query: "build box", + isInSubmenu: false, + projectSearchItems: [ + { + kind: "action", + value: "project:t3code", + title: "T3 Code", + searchTerms: metadata.searchTerms, + icon: null, + run: async () => undefined, + }, + ], + threadSearchItems: [], + }); + expect(filteredGroup?.items).toHaveLength(1); + }); + + it("deduplicates grouped checkouts by environment", () => { + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: remoteEnvironmentId, + title: "T3 Code", + workspaceRoot: "/srv/t3code", + }, + { + environmentId: remoteEnvironmentId, + title: "T3 Code worktree", + workspaceRoot: "/srv/t3code-feature", + }, + ], + locationByEnvironmentId: locations, + }); + + expect(metadata.environmentLabels).toEqual(["Build box"]); + }); + + it("deduplicates distinct environments with the same label", () => { + const secondRemoteEnvironmentId = EnvironmentId.make("environment-build-box-2"); + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: remoteEnvironmentId, + title: "T3 Code", + workspaceRoot: "/srv/t3code", + }, + { + environmentId: secondRemoteEnvironmentId, + title: "T3 Code mirror", + workspaceRoot: "/srv/mirror/t3code", + }, + ], + locationByEnvironmentId: new Map([ + [remoteEnvironmentId, { label: "Build box" }], + [secondRemoteEnvironmentId, { label: "Build box" }], + ]), + }); + + expect(metadata.environmentLabels).toEqual(["Build box"]); + }); + + it("uses a human-readable fallback when presentation data is unavailable", () => { + const metadata = buildCommandPaletteProjectMetadata({ + projects: [ + { + environmentId: remoteEnvironmentId, + title: "T3 Code", + workspaceRoot: "/srv/t3code", + }, + ], + locationByEnvironmentId: new Map(), + }); + + expect(metadata.searchTerms).toContain("Remote"); + expect(metadata.environmentLabels).toEqual(["Remote"]); + }); +}); + describe("reduceCommandPaletteUiState", () => { const closedState = { open: false, mode: "command", openIntent: null } as const; diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index 95313ee940e6..57c1711de158 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -1,6 +1,7 @@ import { threadPullRequestSearchTerms } from "@t3tools/shared/threadPullRequests"; import type { CommandPaletteLinkedThreads } from "../commandPaletteBus"; import { + type EnvironmentId, type FilesystemBrowseEntry, type KeybindingCommand, THREAD_JUMP_KEYBINDING_COMMANDS, @@ -185,6 +186,22 @@ export type CommandPaletteMode = "root" | "root-browse" | "submenu" | "submenu-b // every other surface uses the real title, so overriding it desyncs the icon. export type CommandPaletteProject = Project & { readonly displayName: string }; +export function buildCommandPaletteProjectMetadata(input: { + readonly projects: ReadonlyArray>; + readonly locationByEnvironmentId: ReadonlyMap; +}) { + const searchTerms: string[] = []; + const environmentLabels = new Set(); + + for (const project of input.projects) { + const label = input.locationByEnvironmentId.get(project.environmentId)?.label ?? "Remote"; + searchTerms.push(project.title, project.workspaceRoot, label); + environmentLabels.add(label); + } + + return { searchTerms, environmentLabels: [...environmentLabels] }; +} + export function buildProjectActionItems(input: { projects: ReadonlyArray; valuePrefix: string; diff --git a/apps/web/src/components/CommandPalette.tsx b/apps/web/src/components/CommandPalette.tsx index c54f919125d3..4a9877f87a02 100644 --- a/apps/web/src/components/CommandPalette.tsx +++ b/apps/web/src/components/CommandPalette.tsx @@ -126,6 +126,7 @@ import { ADDON_ICON_CLASS, browseInputEndPaddingClass, buildBrowseGroups, + buildCommandPaletteProjectMetadata, buildProjectActionItems, buildRootGroups, buildThreadActionItems, @@ -186,6 +187,38 @@ function projectFavicon(project: Project) { return ; } +function ProjectSearchDescription(props: { + readonly environmentLabels: ReadonlyArray; + readonly grouped: boolean; + readonly location: { + readonly kind: "local" | "remote"; + readonly label: string; + readonly machine: EnvironmentMachineKind; + }; + readonly workspaceRoot: string; +}) { + if (!props.grouped) { + return ( + + + {props.location.kind === "remote" ? ( + + ) : null} + {props.location.label} + + + {props.workspaceRoot} + + ); + } + + return {props.environmentLabels.join(" ยท ")}; +} + function getEnvironmentBrowsePlatform(os: string | null | undefined): string { if (os === "windows") { return "Win32"; @@ -1079,15 +1112,43 @@ function OpenCommandPaletteDialog(props: { projects: pickerProjects, valuePrefix: "project", searchTerms: (project) => { - const group = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`); + const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) + ?.memberProjects ?? [project]; + return buildCommandPaletteProjectMetadata({ + projects: members, + locationByEnvironmentId: projectEnvironmentLocationById, + }).searchTerms; + }, + renderDescription: (project) => { + const members = projectGroupByTargetKey.get(`${project.environmentId}:${project.id}`) + ?.memberProjects ?? [project]; + const metadata = buildCommandPaletteProjectMetadata({ + projects: members, + locationByEnvironmentId: projectEnvironmentLocationById, + }); + const location = projectEnvironmentLocationById.get(project.environmentId) ?? { + kind: "remote" as const, + label: "Remote", + machine: "server" as const, + }; return ( - group?.memberProjects.flatMap((member) => [member.title, member.workspaceRoot]) ?? [] + 1} + location={location} + workspaceRoot={project.workspaceRoot} + /> ); }, icon: projectFavicon, runProject: openProjectFromSearch, }), - [openProjectFromSearch, pickerProjects, projectGroupByTargetKey], + [ + openProjectFromSearch, + pickerProjects, + projectEnvironmentLocationById, + projectGroupByTargetKey, + ], ); const projectThreadItems = useMemo( @@ -1176,6 +1237,9 @@ function OpenCommandPaletteDialog(props: { -
-
-

Open a surface

-

- Choose what to show in the right panel. -

-
-
+
+

Open a surface

+
{actions.map((action) => action.available ? ( - // The card is itself a button, so the profile chooser sits beside + // The row is itself a button, so the profile chooser sits beside // it in a wrapper rather than inside it. Hover lives on the - // wrapper: the chooser overlays the card, and a pointer moving - // onto it must not read as leaving the card. + // wrapper: the chooser overlays the row, and a pointer moving + // onto it must not read as leaving the row.
- {action.shortcut} - - {actionIcon(action)} - {action.label} - - - {action.description} + {actionIcon(action, "size-4")} + 1 && "pr-7", + )} + > + {action.label} + {action.shortcut} {/* - Same choice the tab bar's "+" menu offers: the card opens the + Same choice the tab bar's "+" menu offers: the row opens the default profile, the chevron picks another. Only worth showing once there is something to choose between. */} @@ -574,7 +555,7 @@ function RightPanelEmptyState(props: { render={
) : ( -
- {action.shortcut} - - {actionIcon(action)} - {action.label} - - - {action.disabledReason} - -
+ reason={action.disabledReason} + trigger={ +
+ {actionIcon(action, "size-4")} + {action.label} + {action.shortcut} +
+ } + /> ), )}
diff --git a/apps/web/src/components/ThreadCommandSubtitle.tsx b/apps/web/src/components/ThreadCommandSubtitle.tsx index 32c6c0373438..275105a79099 100644 --- a/apps/web/src/components/ThreadCommandSubtitle.tsx +++ b/apps/web/src/components/ThreadCommandSubtitle.tsx @@ -36,6 +36,7 @@ function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktre export function ThreadCommandSubtitle(props: { project: ProjectFaviconProject | null; projectTitle: string | null; + environmentLabel?: string | null; branch: string | null; worktreePath: string | null; isCurrent: boolean; @@ -69,6 +70,12 @@ export function ThreadCommandSubtitle(props: { ) : null} {projectLabel} + {props.environmentLabel ? ( + <> + + {props.environmentLabel} + + ) : null} ) : null} diff --git a/apps/web/src/components/media/MediaVideoPlayer.tsx b/apps/web/src/components/media/MediaVideoPlayer.tsx index 90c18984e17b..cc3185a3a77e 100644 --- a/apps/web/src/components/media/MediaVideoPlayer.tsx +++ b/apps/web/src/components/media/MediaVideoPlayer.tsx @@ -93,11 +93,19 @@ export function MediaVideoPlayer({ const video = videoRef.current; if (!video) return; const pauseWhenHidden = () => { - if (document.hidden) video.pause(); + // Native fullscreen can hide the inline page while this video is still visible. + const fullscreen = + document.fullscreenElement?.contains(video) || + ("webkitDisplayingFullscreen" in video && video.webkitDisplayingFullscreen === true); + if (document.hidden && !fullscreen) video.pause(); }; document.addEventListener("visibilitychange", pauseWhenHidden); + document.addEventListener("fullscreenchange", pauseWhenHidden); + video.addEventListener("webkitendfullscreen", pauseWhenHidden); return () => { document.removeEventListener("visibilitychange", pauseWhenHidden); + document.removeEventListener("fullscreenchange", pauseWhenHidden); + video.removeEventListener("webkitendfullscreen", pauseWhenHidden); video.pause(); }; }, [src, failed, loadAttempt]); diff --git a/apps/web/src/components/ui/toast.tsx b/apps/web/src/components/ui/toast.tsx index 0f6483c2ae67..d5e28441dc6b 100644 --- a/apps/web/src/components/ui/toast.tsx +++ b/apps/web/src/components/ui/toast.tsx @@ -560,7 +560,7 @@ function Toasts({ position }: { position: ToastPosition }) { settings.glassOpacity); useEffect(() => { - document.documentElement.style.setProperty("--glass-opacity", `${glassOpacity}%`); + const style = document.documentElement.style; + style.setProperty("--glass-opacity", `${glassOpacity}%`); + if (glassOpacity === 100) { + style.setProperty("--glass-blur", "0px"); + } else { + style.removeProperty("--glass-blur"); + } }, [glassOpacity]); return null; diff --git a/patches/react-native-reanimated@4.5.1.patch b/patches/react-native-reanimated@4.5.1.patch new file mode 100644 index 000000000000..6bf44826d4fd --- /dev/null +++ b/patches/react-native-reanimated@4.5.1.patch @@ -0,0 +1,69 @@ +diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +index 86035915aa330d2011e7c3027ae315c689e40f58..3d43e949550e1bc1311c38d11a7920c237a41018 100644 +--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h ++++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxyCommon.h +@@ -22,6 +22,7 @@ struct LayoutAnimation { + Tag parentTag; + std::optional opacity; + bool isViewAlreadyMounted = false; ++ bool isExitingWhenSettled = false; + int count = 1; + LayoutAnimation &operator=(const LayoutAnimation &other) = default; + +diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp +index bc62f8fc31a1d3b08353df514bf4f954c6279ceb..0c16264538a13ae8b7bd4fd11e1227d8580c5c65 100644 +--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp ++++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Experimental.cpp +@@ -290,6 +290,7 @@ std::optional LayoutAnimationsProxy_Experimental::endLayoutAnimation( + if (--layoutAnimation.count > 0) { + return {}; + } ++ layoutAnimation.isExitingWhenSettled = shouldRemove; + maybeSettledAnimationTags_.insert(tag); + auto surfaceId = layoutAnimation.finalView.surfaceId; + +@@ -407,7 +408,8 @@ void LayoutAnimationsProxy_Experimental::addOngoingAnimations(SurfaceId surfaceI + + const auto layoutAnimationIt = layoutAnimations_.find(tag); + +- if (layoutAnimationIt == layoutAnimations_.end() || layoutAnimationIt->second.isSettled()) { ++ if (layoutAnimationIt == layoutAnimations_.end() || ++ (layoutAnimationIt->second.isSettled() && layoutAnimationIt->second.isExitingWhenSettled)) { + continue; + } + +@@ -554,6 +556,8 @@ void LayoutAnimationsProxy_Experimental::maybeCancelAnimation(const int tag) con + } + if (layoutAnimationIt->second.isSettled()) { + // Already settled - cleanupAnimations will erase it together with its updateMap entry. ++ // Do not flush a pending Update after the caller queues this view for removal. ++ layoutAnimationIt->second.isExitingWhenSettled = true; + return; + } + layoutAnimations_.erase(layoutAnimationIt); +diff --git a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +index 9402e3e5ba4859ed344901b8b5884b9f708dd82b..2b1e4294ef798b7b6aabd04cc6663aae8297d034 100644 +--- a/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp ++++ b/Common/cpp/reanimated/LayoutAnimations/LayoutAnimationsProxy_Legacy.cpp +@@ -119,6 +119,7 @@ std::optional LayoutAnimationsProxy_Legacy::endLayoutAnimation(int ta + if (--layoutAnimation.count > 0) { + return {}; + } ++ layoutAnimation.isExitingWhenSettled = shouldRemove; + maybeSettledAnimationTags_.insert(tag); + auto surfaceId = layoutAnimation.finalView.surfaceId; + +@@ -414,12 +415,7 @@ void LayoutAnimationsProxy_Legacy::addOngoingAnimations(SurfaceId surfaceId, Sha + auto layoutAnimationIt = layoutAnimations_.find(tag); + + if (layoutAnimationIt == layoutAnimations_.end() || +- // A settled animation is normally cleaned up without applying further +- // updates. The exception is a flaky entering animation whose opacity was +- // never restored (the view wasn't mounted in time) - we still need to +- // apply that pending opacity, otherwise the view stays invisible. Only +- // entering animations carry an opacity value. +- (layoutAnimationIt->second.isSettled() && !layoutAnimationIt->second.opacity.has_value())) { ++ (layoutAnimationIt->second.isSettled() && layoutAnimationIt->second.isExitingWhenSettled)) { + continue; + } + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bebd36cec2e8..2cba4e1a8771 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -103,6 +103,7 @@ patchedDependencies: react-native-gesture-handler@2.32.0: 96573c000f7fe56b5abfa13e2e5f0d065907e674cb8e2300155226d7c9874398 react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 + react-native-reanimated@4.5.1: a23baea5d82cbf1254720110aef6671e39d57a425f8be446756f91ab806eb72c react-native-screens@4.26.2: 8156dd0f3407822404793cfdaa95639a36b62102f4507c981b8be83600bb382d uniwind@1.11.0: 17d92be2eec71bb6396b402e8d034968e54b28746876d7977cb3139655f42b90 @@ -429,7 +430,7 @@ importers: version: 0.2.2(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-keyboard-controller: specifier: 1.21.13 - version: 1.21.13(patch_hash=6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787)(react-native-reanimated@4.5.1(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 1.21.13(patch_hash=6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787)(react-native-reanimated@4.5.1(patch_hash=a23baea5d82cbf1254720110aef6671e39d57a425f8be446756f91ab806eb72c)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-markdown: specifier: ^0.5.0 version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -438,7 +439,7 @@ importers: version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-reanimated: specifier: 4.5.1 - version: 4.5.1(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.5.1(patch_hash=a23baea5d82cbf1254720110aef6671e39d57a425f8be446756f91ab806eb72c)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-safe-area-context: specifier: ~5.7.0 version: 5.7.0(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -20932,12 +20933,12 @@ snapshots: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-keyboard-controller@1.21.13(patch_hash=6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787)(react-native-reanimated@4.5.1(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): - dependencies: + ? react-native-keyboard-controller@1.21.13(patch_hash=6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787)(react-native-reanimated@4.5.1(patch_hash=a23baea5d82cbf1254720110aef6671e39d57a425f8be446756f91ab806eb72c)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + : dependencies: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-is-edge-to-edge: 1.3.1(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-reanimated: 4.5.1(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.5.1(patch_hash=a23baea5d82cbf1254720110aef6671e39d57a425f8be446756f91ab806eb72c)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: @@ -20952,7 +20953,7 @@ snapshots: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-reanimated@4.5.1(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-reanimated@4.5.1(patch_hash=a23baea5d82cbf1254720110aef6671e39d57a425f8be446756f91ab806eb72c)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index ae279419d428..8a4bf972f82f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -179,6 +179,8 @@ patchedDependencies: react-native-gesture-handler@2.32.0: patches/react-native-gesture-handler@2.32.0.patch react-native-keyboard-controller@1.21.13: patches/react-native-keyboard-controller@1.21.13.patch react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch + # Preserve the final layout frame. Backport of [#10171](https://github.com/software-mansion/react-native-reanimated/pull/10171). + react-native-reanimated@4.5.1: patches/react-native-reanimated@4.5.1.patch react-native-screens@4.26.2: patches/react-native-screens@4.26.2.patch uniwind@1.11.0: patches/uniwind@1.11.0.patch