-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(mobile): Stabilize thread feed layout and Markdown rendering #4036
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d007a73
b5b2ecd
3c97855
5fcfe27
8b0ce3c
6abdfd3
775d0b1
7f53a54
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,7 +17,7 @@ import type { | |
| import { formatElapsed } from "@t3tools/shared/orchestrationTiming"; | ||
| import * as Haptics from "expo-haptics"; | ||
| import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; | ||
| import { Platform, View, type GestureResponderEvent } from "react-native"; | ||
| import { Platform, View, type GestureResponderEvent, type LayoutChangeEvent } from "react-native"; | ||
| import { KeyboardController, KeyboardStickyView } from "react-native-keyboard-controller"; | ||
| import Animated, { FadeInDown, FadeOut, LinearTransition } from "react-native-reanimated"; | ||
| import { useSafeAreaInsets } from "react-native-safe-area-context"; | ||
|
|
@@ -176,6 +176,10 @@ function useStreamingHaptics(threadId: ThreadId, feed: ReadonlyArray<ThreadFeedE | |
| // overlay). Matches the rendered pill: pt-2 + pb-2 (16) wrapping a bordered | ||
| // px-3/py-2 row (~36), so ~52 — keep it in sync with WorkingDurationPill. | ||
| const WORKING_INDICATOR_HEIGHT = 52; | ||
| // Extra list end-inset so the last message rests above the floating composer | ||
| // instead of flush against (or under) it. Applied via heightAdjustment so both | ||
| // the pre-layout estimate and the measured overlay height pick it up. | ||
| const THREAD_FEED_COMPOSER_GAP = 24; | ||
|
|
||
| const WorkingDurationPill = memo(function WorkingDurationPill(props: { | ||
| readonly startedAt: string; | ||
|
|
@@ -248,6 +252,24 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | |
| const composerOverlapHeight = composerChrome + composerBottomInset; | ||
| const activeWorkIndicatorHeight = props.activeWorkStartedAt ? WORKING_INDICATOR_HEIGHT : 0; | ||
| const estimatedOverlayHeight = composerOverlapHeight + activeWorkIndicatorHeight; | ||
| const [composerOverlayHeight, setComposerOverlayHeight] = useState(estimatedOverlayHeight); | ||
| const estimatedOverlayHeightRef = useRef(estimatedOverlayHeight); | ||
| useEffect(() => { | ||
| estimatedOverlayHeightRef.current = estimatedOverlayHeight; | ||
| }, [estimatedOverlayHeight]); | ||
| useEffect(() => { | ||
| // A measured overlay can include approval or input cards that are not part | ||
| // of this baseline estimate. Keep that larger measurement until layout | ||
| // reports a new height, while ensuring chrome changes never under-estimate | ||
| // the composer inset. | ||
| setComposerOverlayHeight((current) => Math.max(current, estimatedOverlayHeight)); | ||
|
juliusmarminge marked this conversation as resolved.
|
||
| }, [estimatedOverlayHeight]); | ||
| useEffect(() => { | ||
| // Layout callbacks from the previous thread can arrive after navigation. | ||
| // Reset their larger measurement to this thread's known baseline; its own | ||
| // overlay layout will immediately replace it if it contains extra cards. | ||
| setComposerOverlayHeight(estimatedOverlayHeightRef.current); | ||
| }, [selectedThreadKey]); | ||
| // The overlay's measured height includes the home-indicator inset (the | ||
| // composer pads it), but contentInsetAdjustmentBehavior="automatic" makes | ||
| // UIKit add the safe-area bottom to the content inset AGAIN — leaving a | ||
|
|
@@ -257,11 +279,22 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | |
| // its end-scroll math matches the real resting position. | ||
| const nativeInsetOvercount = | ||
| props.usesAutomaticContentInsets === true && Platform.OS === "ios" ? insets.bottom : 0; | ||
| // heightAdjustment is added to measured composer height. Keep the safe-area | ||
| // under-report (UIKit automatic insets) and add breathing room above chrome. | ||
| const composerEndHeightAdjustment = THREAD_FEED_COMPOSER_GAP - nativeInsetOvercount; | ||
| const { contentInsetEndAdjustment, onComposerLayout } = useKeyboardChatComposerInset( | ||
| listRef, | ||
| composerOverlayRef, | ||
| Math.max(0, estimatedOverlayHeight - nativeInsetOvercount), | ||
| -nativeInsetOvercount, | ||
| Math.max(0, estimatedOverlayHeight + composerEndHeightAdjustment), | ||
| composerEndHeightAdjustment, | ||
| ); | ||
| const handleComposerLayout = useCallback( | ||
| (event: LayoutChangeEvent) => { | ||
| onComposerLayout(event); | ||
| const height = event.nativeEvent.layout.height; | ||
| setComposerOverlayHeight((current) => (Math.abs(current - height) > 1 ? height : current)); | ||
|
juliusmarminge marked this conversation as resolved.
|
||
| }, | ||
| [onComposerLayout], | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale composer layout after navigationMedium Severity The Reviewed by Cursor Bugbot for commit 7f53a54. Configure here. |
||
| ); | ||
| const { freeze, scrollMessageToEnd } = useKeyboardScrollToEnd({ listRef }); | ||
| const showContent = props.showContent ?? true; | ||
|
|
@@ -408,6 +441,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | |
| freeze={freeze} | ||
| anchorMessageId={anchorMessageId} | ||
| contentInsetEndAdjustment={contentInsetEndAdjustment} | ||
| contentInsetEndEstimate={composerOverlayHeight + THREAD_FEED_COMPOSER_GAP} | ||
| contentTopInset={0} | ||
| contentBottomInset={estimatedOverlayHeight} | ||
| contentMaxWidth={contentMaxWidth} | ||
|
|
@@ -430,7 +464,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 | ||
| pushes the resting content floor up by the same amount. */} | ||
| <View ref={composerOverlayRef} onLayout={onComposerLayout} className="w-full"> | ||
| <View ref={composerOverlayRef} onLayout={handleComposerLayout} className="w-full"> | ||
| <Animated.View | ||
| className="w-full self-center" | ||
| layout={LinearTransition.duration(220)} | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.