| author | |
| committer | |
| log | f15680bd3c08a154bacb9b4d163cc8608a310970 |
| tree | 680e35715f2ea10d0d24cca736b1004e0d85c1c8 |
| parent | 2daff8c25adf701fddd062ae93b94cc57d420ec2 |
| signature | Signed by PGP key B5690EEEBB952194 |
14 files changed, 2978 insertions(+), 894 deletions(-)
apps/mobile/src/features/threads/ThreadComposer.tsx+178-119| ... | @@ -9,7 +9,16 @@ import type { | ... | @@ -9,7 +9,16 @@ import type { |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; | 10 | import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; |
| 11 | import type { ReactNode } from "react"; | 11 | import type { ReactNode } from "react"; |
| 12 | import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react"; | 12 | import { |
| 13 | memo, | ||
| 14 | useCallback, | ||
| 15 | useEffect, | ||
| 16 | useLayoutEffect, | ||
| 17 | useMemo, | ||
| 18 | useRef, | ||
| 19 | useState, | ||
| 20 | type RefObject, | ||
| 21 | } from "react"; | ||
| 13 | import { ActivityIndicator, Image, Platform, Pressable, View, type ViewStyle } from "react-native"; | 22 | import { ActivityIndicator, Image, Platform, Pressable, View, type ViewStyle } from "react-native"; |
| 14 | import ImageViewing from "react-native-image-viewing"; | 23 | import ImageViewing from "react-native-image-viewing"; |
| 15 | import Animated, { | 24 | import Animated, { |
| ... | @@ -18,6 +27,9 @@ import Animated, { | ... | @@ -18,6 +27,9 @@ import Animated, { |
| 18 | FadeOut, | 27 | FadeOut, |
| 19 | FadeOutDown, | 28 | FadeOutDown, |
| 20 | LinearTransition, | 29 | LinearTransition, |
| 30 | useAnimatedStyle, | ||
| 31 | useSharedValue, | ||
| 32 | withTiming, | ||
| 21 | } from "react-native-reanimated"; | 33 | } from "react-native-reanimated"; |
| 22 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | 34 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; |
| 23 | import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration"; | 35 | import { armAgentAwarenessLiveActivityForLocalWork } from "../agent-awareness/remoteRegistration"; |
| ... | @@ -31,7 +43,6 @@ import { | ... | @@ -31,7 +43,6 @@ import { |
| 31 | ComposerInlineControl, | 43 | ComposerInlineControl, |
| 32 | ComposerToolbarButton, | 44 | ComposerToolbarButton, |
| 33 | ComposerToolbarRow, | 45 | ComposerToolbarRow, |
| 34 | ComposerToolbarScroller, | ||
| 35 | } from "../../components/ComposerToolbar"; | 46 | } from "../../components/ComposerToolbar"; |
| 36 | import { ControlPill } from "../../components/ControlPill"; | 47 | import { ControlPill } from "../../components/ControlPill"; |
| 37 | import { ProviderIcon } from "../../components/ProviderIcon"; | 48 | import { ProviderIcon } from "../../components/ProviderIcon"; |
| ... | @@ -104,14 +115,17 @@ export interface ThreadComposerProps { | ... | @@ -104,14 +115,17 @@ export interface ThreadComposerProps { |
| 104 | * iOS 26+ devices and keeps the existing opaque fallback elsewhere. | 115 | * iOS 26+ devices and keeps the existing opaque fallback elsewhere. |
| 105 | * Exported so NewTaskDraftScreen can render the same composer chrome. | 116 | * Exported so NewTaskDraftScreen can render the same composer chrome. |
| 106 | */ | 117 | */ |
| 107 | // One timing for every piece of the expanded↔compact morph so the surface, | 118 | // The bottom-anchored dock position and clipped surface height use the same |
| 108 | // toolbar, and siblings move together instead of popping between layouts. | 119 | // transition so the card grows upward without exposing its final-size content. |
| 109 | // Android gets NO layout transition: the composer rides the keyboard via | 120 | // Android gets NO layout transition: the composer rides the keyboard via |
| 110 | // KeyboardStickyView (frame-synced to the IME), and a time-based morph | 121 | // KeyboardStickyView (frame-synced to the IME), and a time-based morph |
| 111 | // running alongside that translate reads as jitter. Snapping the layout and | 122 | // running alongside that translate reads as jitter. Snapping the layout and |
| 112 | // letting the keyboard-synced slide be the only motion looks native there. | 123 | // letting the keyboard-synced slide be the only motion looks native there. |
| 113 | const COMPOSER_LAYOUT_TRANSITION = | 124 | export const COMPOSER_TRANSITION_DURATION_MS = 220; |
| 114 | Platform.OS === "android" ? undefined : LinearTransition.duration(220); | 125 | export const COMPOSER_LAYOUT_TRANSITION = |
| 126 | Platform.OS === "android" | ||
| 127 | ? undefined | ||
| 128 | : LinearTransition.duration(COMPOSER_TRANSITION_DURATION_MS); | ||
| 115 | 129 | ||
| 116 | export function ComposerSurface(props: { | 130 | export function ComposerSurface(props: { |
| 117 | readonly children: ReactNode; | 131 | readonly children: ReactNode; |
| ... | @@ -119,29 +133,53 @@ export function ComposerSurface(props: { | ... | @@ -119,29 +133,53 @@ export function ComposerSurface(props: { |
| 119 | /** Existing thread composers morph between pill and card layouts. */ | 133 | /** Existing thread composers morph between pill and card layouts. */ |
| 120 | readonly animateLayout?: boolean; | 134 | readonly animateLayout?: boolean; |
| 121 | }) { | 135 | }) { |
| 122 | // A box shadow follows the rounded surface even when the glass is transparent. | 136 | const targetBorderRadius = |
| 123 | // Keep it outside the clipped content so the shadow can extend past the edge. | 137 | typeof props.style.borderRadius === "number" ? props.style.borderRadius : 0; |
| 138 | const animatedBorderRadius = useSharedValue(targetBorderRadius); | ||
| 139 | const shouldAnimate = props.animateLayout !== false && Platform.OS !== "android"; | ||
| 140 | useLayoutEffect(() => { | ||
| 141 | animatedBorderRadius.value = shouldAnimate | ||
| 142 | ? withTiming(targetBorderRadius, { | ||
| 143 | duration: COMPOSER_TRANSITION_DURATION_MS, | ||
| 144 | }) | ||
| 145 | : targetBorderRadius; | ||
| 146 | }, [animatedBorderRadius, shouldAnimate, targetBorderRadius]); | ||
| 147 | const animatedShapeStyle = useAnimatedStyle(() => ({ | ||
| 148 | borderRadius: animatedBorderRadius.value, | ||
| 149 | })); | ||
| 150 | |||
| 151 | // Clipping here keeps the expanded toolbar inside the glass while the | ||
| 152 | // bottom-anchored host owns the geometry transition. | ||
| 124 | return ( | 153 | return ( |
| 125 | <Animated.View | 154 | <Animated.View |
| 126 | className="shadow-[0_6px_28px] shadow-adaptive-black-a15-a35" | 155 | className="shadow-[0_6px_28px] shadow-adaptive-black-a15-a35" |
| 127 | layout={props.animateLayout === false ? undefined : COMPOSER_LAYOUT_TRANSITION} | 156 | layout={props.animateLayout === false ? undefined : COMPOSER_LAYOUT_TRANSITION} |
| 128 | style={{ | 157 | style={[ |
| 129 | borderRadius: props.style.borderRadius, | 158 | animatedShapeStyle, |
| 130 | // Android versions before 9 do not support outset box shadows. | 159 | { |
| 131 | elevation: Platform.OS === "android" && Platform.Version < 28 ? 10 : undefined, | 160 | overflow: "hidden", |
| 132 | }} | 161 | // Android versions before 9 do not support outset box shadows. |
| 162 | elevation: Platform.OS === "android" && Platform.Version < 28 ? 10 : undefined, | ||
| 163 | }, | ||
| 164 | ]} | ||
| 133 | > | 165 | > |
| 134 | <GlassSurface | 166 | <GlassSurface |
| 135 | chrome="none" | 167 | chrome="none" |
| 136 | fallbackClassName="border border-border bg-card-translucent" | 168 | fallbackClassName="border border-border bg-card-translucent" |
| 137 | glassEffectStyle="regular" | 169 | glassEffectStyle="regular" |
| 138 | // The composer is a passive material containing interactive controls. | 170 | // The composer is a passive material containing interactive controls. |
| 139 | // Expo GlassView defaults to non-interactive and both layouts share it. | 171 | // Keep native glass out of the interactive content's layout path. |
| 172 | pointerEvents="none" | ||
| 140 | tintColor="transparent" | 173 | tintColor="transparent" |
| 141 | style={props.style} | 174 | style={{ |
| 175 | position: "absolute", | ||
| 176 | inset: 0, | ||
| 177 | borderRadius: targetBorderRadius, | ||
| 178 | }} | ||
| 142 | > | 179 | > |
| 143 | {props.children} | 180 | {null} |
| 144 | </GlassSurface> | 181 | </GlassSurface> |
| 182 | <View style={props.style}>{props.children}</View> | ||
| 145 | </Animated.View> | 183 | </Animated.View> |
| 146 | ); | 184 | ); |
| 147 | } | 185 | } |
| ... | @@ -254,9 +292,8 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | ... | @@ -254,9 +292,8 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 254 | const isExpanded = isFocused || settingsSheetPresentation.isActive; | 292 | const isExpanded = isFocused || settingsSheetPresentation.isActive; |
| 255 | const canSend = hasContent; | 293 | const canSend = hasContent; |
| 256 | 294 | ||
| 257 | // Notify the parent from the derived value, not focus events: the parent | 295 | // Keep the parent's feed inset synchronized while the settings sheet keeps |
| 258 | // sizes the feed inset from this, and blur-during-sheet would otherwise | 296 | // the composer expanded between editor focus events. |
| 259 | // report collapsed while the composer still renders expanded. | ||
| 260 | useEffect(() => { | 297 | useEffect(() => { |
| 261 | onExpandedChange?.(isExpanded); | 298 | onExpandedChange?.(isExpanded); |
| 262 | }, [isExpanded, onExpandedChange]); | 299 | }, [isExpanded, onExpandedChange]); |
| ... | @@ -279,13 +316,17 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | ... | @@ -279,13 +316,17 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 279 | const onEditorFocusChange = props.onEditorFocusChange; | 316 | const onEditorFocusChange = props.onEditorFocusChange; |
| 280 | const handleFocus = useCallback(() => { | 317 | const handleFocus = useCallback(() => { |
| 281 | setIsFocused(true); | 318 | setIsFocused(true); |
| 319 | onExpandedChange?.(true); | ||
| 282 | onEditorFocusChange?.(true); | 320 | onEditorFocusChange?.(true); |
| 283 | }, [onEditorFocusChange]); | 321 | }, [onEditorFocusChange, onExpandedChange]); |
| 284 | 322 | ||
| 285 | const handleBlur = useCallback(() => { | 323 | const handleBlur = useCallback(() => { |
| 286 | setIsFocused(false); | 324 | setIsFocused(false); |
| 325 | if (!settingsSheetPresentation.isActive) { | ||
| 326 | onExpandedChange?.(false); | ||
| 327 | } | ||
| 287 | onEditorFocusChange?.(false); | 328 | onEditorFocusChange?.(false); |
| 288 | }, [onEditorFocusChange]); | 329 | }, [onEditorFocusChange, onExpandedChange, settingsSheetPresentation.isActive]); |
| 289 | const showStopAction = | 330 | const showStopAction = |
| 290 | props.selectedThread.session?.status === "running" || | 331 | props.selectedThread.session?.status === "running" || |
| 291 | props.selectedThread.session?.status === "starting"; | 332 | props.selectedThread.session?.status === "starting"; |
| ... | @@ -445,7 +486,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | ... | @@ -445,7 +486,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 445 | return ( | 486 | return ( |
| 446 | <Animated.View | 487 | <Animated.View |
| 447 | className="px-4" | 488 | className="px-4" |
| 448 | layout={COMPOSER_LAYOUT_TRANSITION} | ||
| 449 | style={{ | 489 | style={{ |
| 450 | paddingTop: isExpanded ? 8 : 6, | 490 | paddingTop: isExpanded ? 8 : 6, |
| 451 | paddingBottom: (props.bottomInset ?? 0) + (isExpanded ? 8 : 6), | 491 | paddingBottom: (props.bottomInset ?? 0) + (isExpanded ? 8 : 6), |
| ... | @@ -460,7 +500,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | ... | @@ -460,7 +500,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 460 | /> | 500 | /> |
| 461 | <Animated.View | 501 | <Animated.View |
| 462 | className="relative w-full self-center" | 502 | className="relative w-full self-center" |
| 463 | layout={COMPOSER_LAYOUT_TRANSITION} | ||
| 464 | style={{ maxWidth: props.contentMaxWidth }} | 503 | style={{ maxWidth: props.contentMaxWidth }} |
| 465 | > | 504 | > |
| 466 | {composerMenu.trigger && composerMenu.items.length > 0 ? ( | 505 | {composerMenu.trigger && composerMenu.items.length > 0 ? ( |
| ... | @@ -493,12 +532,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | ... | @@ -493,12 +532,11 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 493 | paddingTop: 14, | 532 | paddingTop: 14, |
| 494 | } | 533 | } |
| 495 | : { | 534 | : { |
| 496 | borderRadius: 999, | 535 | // Keep the numeric radius close to the expanded card so the |
| 536 | // shape morph stays bounded while rendering as a capsule. | ||
| 537 | borderRadius: 27, | ||
| 497 | overflow: "hidden" as const, | 538 | overflow: "hidden" as const, |
| 498 | flexDirection: "row" as const, | 539 | paddingHorizontal: 14, |
| 499 | alignItems: "center" as const, | ||
| 500 | paddingLeft: 18, | ||
| 501 | paddingRight: 5, | ||
| 502 | paddingVertical: 5, | 540 | paddingVertical: 5, |
| 503 | } | 541 | } |
| 504 | } | 542 | } |
| ... | @@ -518,95 +556,124 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | ... | @@ -518,95 +556,124 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 518 | </Animated.View> | 556 | </Animated.View> |
| 519 | ) : null} | 557 | ) : null} |
| 520 | 558 | ||
| 521 | <View className={isExpanded ? undefined : "min-w-0 flex-1"}> | 559 | <View className={isExpanded ? undefined : "flex-row items-center"}> |
| 522 | <ComposerEditor | 560 | <View className={isExpanded ? undefined : "min-w-0 flex-1"}> |
| 523 | ref={inputRef} | 561 | <ComposerEditor |
| 524 | multiline | 562 | ref={inputRef} |
| 525 | value={props.draftMessage} | 563 | multiline |
| 526 | skills={selectedProviderStatus?.skills ?? []} | 564 | value={props.draftMessage} |
| 527 | selection={composerMenu.selection} | 565 | skills={selectedProviderStatus?.skills ?? []} |
| 528 | onChangeText={props.onChangeDraftMessage} | 566 | selection={composerMenu.selection} |
| 529 | onSelectionChange={composerMenu.onSelectionChange} | 567 | onChangeText={props.onChangeDraftMessage} |
| 530 | onPasteImages={(uris) => void props.onNativePasteImages(uris)} | 568 | onSelectionChange={composerMenu.onSelectionChange} |
| 531 | placeholder={props.placeholder} | 569 | onPasteImages={(uris) => void props.onNativePasteImages(uris)} |
| 532 | onFocus={handleFocus} | 570 | placeholder={props.placeholder} |
| 533 | onBlur={handleBlur} | 571 | onFocus={handleFocus} |
| 534 | onSubmit={handleSend} | 572 | onBlur={handleBlur} |
| 535 | scrollEnabled={isExpanded} | 573 | onSubmit={handleSend} |
| 536 | // Android: collapsed single line centers natively (gravity) in | 574 | scrollEnabled={isExpanded} |
| 537 | // a pill-height box matching the send button; iOS keeps insets. | 575 | // Android: collapsed single line centers natively (gravity) in |
| 538 | singleLineCentered={!isExpanded} | 576 | // a pill-height box matching the send button; iOS keeps insets. |
| 539 | contentInsetVertical={isExpanded || Platform.OS === "android" ? 0 : 6} | 577 | singleLineCentered={!isExpanded} |
| 540 | style={ | 578 | contentInsetVertical={isExpanded || Platform.OS === "android" ? 0 : 6} |
| 541 | isExpanded | 579 | style={ |
| 542 | ? { | 580 | isExpanded |
| 543 | minHeight: 72, | 581 | ? { |
| 544 | maxHeight: 160, | 582 | minHeight: 72, |
| 545 | paddingHorizontal: 4, | 583 | maxHeight: 160, |
| 546 | paddingVertical: 4, | 584 | paddingHorizontal: 4, |
| 547 | } | 585 | paddingVertical: 4, |
| 548 | : { | 586 | } |
| 549 | height: 36, | 587 | : { |
| 550 | } | 588 | height: 36, |
| 551 | } | 589 | paddingHorizontal: 4, |
| 552 | textStyle={{ | 590 | } |
| 553 | ...bodyText, | 591 | } |
| 554 | color: foregroundColor, | 592 | textStyle={{ |
| 555 | }} | 593 | ...bodyText, |
| 556 | /> | 594 | color: foregroundColor, |
| 557 | </View> | 595 | }} |
| 558 | {!isExpanded && props.draftAttachments.length > 0 ? ( | 596 | /> |
| 559 | <View className="flex-row gap-1 pl-1"> | ||
| 560 | {props.draftAttachments.slice(0, 3).map((image) => ( | ||
| 561 | <Pressable key={image.id} onPress={() => onPressImage(image.previewUri)}> | ||
| 562 | <Image | ||
| 563 | source={{ uri: image.previewUri }} | ||
| 564 | className="size-[30px] rounded-lg bg-subtle" | ||
| 565 | resizeMode="cover" | ||
| 566 | /> | ||
| 567 | </Pressable> | ||
| 568 | ))} | ||
| 569 | {props.draftAttachments.length > 3 ? ( | ||
| 570 | <View className="size-[30px] items-center justify-center rounded-lg bg-subtle-strong"> | ||
| 571 | <Text className="text-foreground-muted text-2xs font-t3-bold"> | ||
| 572 | +{props.draftAttachments.length - 3} | ||
| 573 | </Text> | ||
| 574 | </View> | ||
| 575 | ) : null} | ||
| 576 | </View> | 597 | </View> |
| 577 | ) : null} | 598 | {!isExpanded && props.draftAttachments.length > 0 ? ( |
| 578 | {!isExpanded ? ( | 599 | <View className="flex-row gap-1 pl-1"> |
| 579 | <Animated.View entering={FadeIn.duration(180)} exiting={FadeOut.duration(100)}> | 600 | {props.draftAttachments.slice(0, 3).map((image) => ( |
| 580 | {showStopAction ? ( | 601 | <Pressable key={image.id} onPress={() => onPressImage(image.previewUri)}> |
| 581 | <ControlPill icon="stop.fill" variant="danger" onPress={props.onStopThread} /> | 602 | <Image |
| 582 | ) : ( | 603 | source={{ uri: image.previewUri }} |
| 583 | <ControlPill | 604 | className="size-[30px] rounded-lg bg-subtle" |
| 584 | icon="arrow.up" | 605 | resizeMode="cover" |
| 585 | variant="primary" | 606 | /> |
| 586 | disabled={!canSend} | 607 | </Pressable> |
| 587 | onPress={handleSend} | 608 | ))} |
| 588 | /> | 609 | {props.draftAttachments.length > 3 ? ( |
| 589 | )} | 610 | <View className="size-[30px] items-center justify-center rounded-lg bg-subtle-strong"> |
| 590 | </Animated.View> | 611 | <Text className="text-foreground-muted text-2xs font-t3-bold"> |
| 591 | ) : null} | 612 | +{props.draftAttachments.length - 3} |
| 592 | {isExpanded ? ( | 613 | </Text> |
| 614 | </View> | ||
| 615 | ) : null} | ||
| 616 | </View> | ||
| 617 | ) : null} | ||
| 618 | {!isExpanded ? ( | ||
| 619 | <Animated.View entering={FadeIn.duration(180)} exiting={FadeOut.duration(100)}> | ||
| 620 | {showStopAction ? ( | ||
| 621 | <ControlPill icon="stop.fill" variant="danger" onPress={props.onStopThread} /> | ||
| 622 | ) : ( | ||
| 623 | <ControlPill | ||
| 624 | icon="arrow.up" | ||
| 625 | variant="primary" | ||
| 626 | disabled={!canSend} | ||
| 627 | onPress={handleSend} | ||
| 628 | /> | ||
| 629 | )} | ||
| 630 | </Animated.View> | ||
| 631 | ) : null} | ||
| 632 | </View> | ||
| 633 | <View | ||
| 634 | accessibilityElementsHidden={!isExpanded} | ||
| 635 | collapsable={false} | ||
| 636 | importantForAccessibility={isExpanded ? "auto" : "no-hide-descendants"} | ||
| 637 | pointerEvents={isExpanded ? "auto" : "none"} | ||
| 638 | style={ | ||
| 639 | isExpanded | ||
| 640 | ? undefined | ||
| 641 | : { | ||
| 642 | height: 0, | ||
| 643 | opacity: 0, | ||
| 644 | overflow: "hidden", | ||
| 645 | } | ||
| 646 | } | ||
| 647 | > | ||
| 593 | <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}> | 648 | <ComposerToolbarRow paddingBottom={0} paddingHorizontal={0} paddingTop={4}> |
| 594 | <ComposerToolbarScroller contentPaddingRight={8}> | 649 | <View className="min-w-0 flex-1 flex-row items-center gap-2"> |
| 595 | <ComposerToolbarButton | 650 | <ComposerToolbarButton |
| 596 | accessibilityLabel="Add attachment" | 651 | accessibilityLabel="Add attachment" |
| 597 | icon="plus" | 652 | icon="plus" |
| 598 | onPress={() => void props.onPickDraftImages()} | 653 | onPress={() => void props.onPickDraftImages()} |
| 599 | showChevron={false} | 654 | showChevron={false} |
| 600 | /> | 655 | /> |
| 601 | <ComposerInlineControl | 656 | <View className="min-w-0 flex-1" style={{ maxWidth: 152 }}> |
| 602 | accessibilityLabel="Model and reasoning settings" | 657 | <ComposerInlineControl |
| 603 | emphasized | 658 | accessibilityLabel="Model and reasoning settings" |
| 604 | iconNode={ | 659 | emphasized |
| 605 | <ProviderIcon provider={currentModelOption?.providerDriver} size={16} /> | 660 | iconNode={ |
| 606 | } | 661 | <ProviderIcon provider={currentModelOption?.providerDriver} size={16} /> |
| 607 | label={currentModelOption?.label ?? currentModelSelection.model} | 662 | } |
| 608 | maxWidth={152} | 663 | label={currentModelOption?.label ?? currentModelSelection.model} |
| 609 | onPress={openSettings} | 664 | maxWidth={152} |
| 665 | onPress={openSettings} | ||
| 666 | /> | ||
| 667 | </View> | ||
| 668 | </View> | ||
| 669 | <View className="shrink-0 flex-row items-center gap-2"> | ||
| 670 | <ComposerToolbarButton | ||
| 671 | accessibilityLabel={sendLabel} | ||
| 672 | icon="arrow.up" | ||
| 673 | variant="primary" | ||
| 674 | disabled={!canSend} | ||
| 675 | onPress={handleSend} | ||
| 676 | showChevron={false} | ||
| 610 | /> | 677 | /> |
| 611 | {showStopAction ? ( | 678 | {showStopAction ? ( |
| 612 | <ComposerToolbarButton | 679 | <ComposerToolbarButton |
| ... | @@ -617,17 +684,9 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer | ... | @@ -617,17 +684,9 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer |
| 617 | showChevron={false} | 684 | showChevron={false} |
| 618 | /> | 685 | /> |
| 619 | ) : null} | 686 | ) : null} |
| 620 | </ComposerToolbarScroller> | 687 | </View> |
| 621 | <ComposerToolbarButton | ||
| 622 | accessibilityLabel={sendLabel} | ||
| 623 | icon="arrow.up" | ||
| 624 | variant="primary" | ||
| 625 | disabled={!canSend} | ||
| 626 | onPress={handleSend} | ||
| 627 | showChevron={false} | ||
| 628 | /> | ||
| 629 | </ComposerToolbarRow> | 688 | </ComposerToolbarRow> |
| 630 | ) : null} | 689 | </View> |
| 631 | </ComposerSurface> | 690 | </ComposerSurface> |
| 632 | 691 | ||
| 633 | {/* Queue count */} | 692 | {/* Queue count */} |
apps/mobile/src/features/threads/ThreadDetailScreen.tsx+160-140| ... | @@ -27,7 +27,6 @@ import { | ... | @@ -27,7 +27,6 @@ import { |
| 27 | useRef, | 27 | useRef, |
| 28 | useState, | 28 | useState, |
| 29 | } from "react"; | 29 | } from "react"; |
| 30 | import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass"; | ||
| 31 | import { | 30 | import { |
| 32 | AppState, | 31 | AppState, |
| 33 | Keyboard, | 32 | Keyboard, |
| ... | @@ -45,13 +44,13 @@ import Animated, { | ... | @@ -45,13 +44,13 @@ import Animated, { |
| 45 | Easing, | 44 | Easing, |
| 46 | FadeInDown, | 45 | FadeInDown, |
| 47 | FadeOut, | 46 | FadeOut, |
| 47 | ReduceMotion, | ||
| 48 | useAnimatedReaction, | 48 | useAnimatedReaction, |
| 49 | useSharedValue, | 49 | useSharedValue, |
| 50 | withTiming, | 50 | withTiming, |
| 51 | } from "react-native-reanimated"; | 51 | } from "react-native-reanimated"; |
| 52 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 52 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 53 | 53 | ||
| 54 | import { ControlPill } from "../../components/ControlPill"; | ||
| 55 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | 54 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 56 | import type { ComposerEditorHandle } from "../../components/ComposerEditor"; | 55 | import type { ComposerEditorHandle } from "../../components/ComposerEditor"; |
| 57 | import type { StatusTone } from "../../components/StatusPill"; | 56 | import type { StatusTone } from "../../components/StatusPill"; |
| ... | @@ -67,6 +66,10 @@ import type { | ... | @@ -67,6 +66,10 @@ import type { |
| 67 | } from "../../lib/threadActivity"; | 66 | } from "../../lib/threadActivity"; |
| 68 | import { PendingApprovalCard } from "./PendingApprovalCard"; | 67 | import { PendingApprovalCard } from "./PendingApprovalCard"; |
| 69 | import { PendingUserInputCard } from "./PendingUserInputCard"; | 68 | import { PendingUserInputCard } from "./PendingUserInputCard"; |
| 69 | import { | ||
| 70 | FLOATING_WORKING_CONTROL_COVERAGE, | ||
| 71 | FloatingWorkingControl, | ||
| 72 | } from "./floating-working-control"; | ||
| 70 | import { | 73 | import { |
| 71 | derivePendingUserInputMaxHeight, | 74 | derivePendingUserInputMaxHeight, |
| 72 | ESTIMATED_KEYBOARD_HEIGHT, | 75 | ESTIMATED_KEYBOARD_HEIGHT, |
| ... | @@ -75,6 +78,8 @@ import { | ... | @@ -75,6 +78,8 @@ import { |
| 75 | import { | 78 | import { |
| 76 | COMPOSER_COLLAPSED_CHROME, | 79 | COMPOSER_COLLAPSED_CHROME, |
| 77 | COMPOSER_EXPANDED_CHROME, | 80 | COMPOSER_EXPANDED_CHROME, |
| 81 | COMPOSER_LAYOUT_TRANSITION, | ||
| 82 | COMPOSER_TRANSITION_DURATION_MS, | ||
| 78 | ThreadComposer, | 83 | ThreadComposer, |
| 79 | } from "./ThreadComposer"; | 84 | } from "./ThreadComposer"; |
| 80 | import { ThreadFeed } from "./ThreadFeed"; | 85 | import { ThreadFeed } from "./ThreadFeed"; |
| ... | @@ -290,6 +295,14 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -290,6 +295,14 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 290 | return null; | 295 | return null; |
| 291 | } | 296 | } |
| 292 | })(); | 297 | })(); |
| 298 | const showWorkingControl = | ||
| 299 | props.activeWorkStartedAt !== null && | ||
| 300 | contentPresentationKind === "ready" && | ||
| 301 | threadSyncPhase === null && | ||
| 302 | props.connectionStateLabel === "connected" && | ||
| 303 | props.activePendingApproval === null && | ||
| 304 | props.activePendingUserInput === null; | ||
| 305 | const floatingWorkingStartedAt = showWorkingControl ? props.activeWorkStartedAt : null; | ||
| 293 | const selectedThreadFeed = props.selectedThreadFeed; | 306 | const selectedThreadFeed = props.selectedThreadFeed; |
| 294 | const composerChrome = composerExpanded ? COMPOSER_EXPANDED_CHROME : COMPOSER_COLLAPSED_CHROME; | 307 | const composerChrome = composerExpanded ? COMPOSER_EXPANDED_CHROME : COMPOSER_COLLAPSED_CHROME; |
| 295 | const composerOverlapHeight = composerChrome + composerBottomInset; | 308 | const composerOverlapHeight = composerChrome + composerBottomInset; |
| ... | @@ -343,6 +356,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -343,6 +356,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 343 | composerOverlayRef, | 356 | composerOverlayRef, |
| 344 | Math.max(0, estimatedOverlayHeight - nativeInsetOvercount), | 357 | Math.max(0, estimatedOverlayHeight - nativeInsetOvercount), |
| 345 | -nativeInsetOvercount, | 358 | -nativeInsetOvercount, |
| 359 | Platform.OS === "ios" ? COMPOSER_TRANSITION_DURATION_MS : 0, | ||
| 346 | ); | 360 | ); |
| 347 | // The expanded questionnaire is an absolute overlay on iOS, so it never | 361 | // The expanded questionnaire is an absolute overlay on iOS, so it never |
| 348 | // changes the measured overlay height (that constancy is what keeps the | 362 | // changes the measured overlay height (that constancy is what keeps the |
| ... | @@ -356,6 +370,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -356,6 +370,15 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 356 | const userInputCardProgress = useSharedValue(1); | 370 | const userInputCardProgress = useSharedValue(1); |
| 357 | const userInputInsetProgress = useSharedValue(1); | 371 | const userInputInsetProgress = useSharedValue(1); |
| 358 | const userInputCardCoverage = useSharedValue(0); | 372 | const userInputCardCoverage = useSharedValue(0); |
| 373 | const floatingControlCoverage = useSharedValue( | ||
| 374 | showWorkingControl ? FLOATING_WORKING_CONTROL_COVERAGE : 0, | ||
| 375 | ); | ||
| 376 | useEffect(() => { | ||
| 377 | floatingControlCoverage.value = withTiming( | ||
| 378 | showWorkingControl ? FLOATING_WORKING_CONTROL_COVERAGE : 0, | ||
| 379 | { duration: 180, reduceMotion: ReduceMotion.System }, | ||
| 380 | ); | ||
| 381 | }, [floatingControlCoverage, showWorkingControl]); | ||
| 359 | // Android renders the expanded card in-flow (it cannot hit-test the iOS | 382 | // Android renders the expanded card in-flow (it cannot hit-test the iOS |
| 360 | // overlay outside the bar's bounds), so its measured overlay height already | 383 | // overlay outside the bar's bounds), so its measured overlay height already |
| 361 | // includes the card — the coverage extra is iOS-only. | 384 | // includes the card — the coverage extra is iOS-only. |
| ... | @@ -366,6 +389,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -366,6 +389,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 366 | useAnimatedReaction( | 389 | useAnimatedReaction( |
| 367 | () => | 390 | () => |
| 368 | contentInsetEndAdjustment.value + | 391 | contentInsetEndAdjustment.value + |
| 392 | floatingControlCoverage.value + | ||
| 369 | (userInputCoverageApplies ? userInputInsetProgress.value * userInputCardCoverage.value : 0), | 393 | (userInputCoverageApplies ? userInputInsetProgress.value * userInputCardCoverage.value : 0), |
| 370 | (value) => { | 394 | (value) => { |
| 371 | combinedContentInsetEndAdjustment.value = value; | 395 | combinedContentInsetEndAdjustment.value = value; |
| ... | @@ -375,20 +399,24 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -375,20 +399,24 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 375 | const { freeze, scrollMessageToEnd } = useKeyboardScrollToEnd({ listRef }); | 399 | const { freeze, scrollMessageToEnd } = useKeyboardScrollToEnd({ listRef }); |
| 376 | const endFollowEnabledRef = useRef(true); | 400 | const endFollowEnabledRef = useRef(true); |
| 377 | endFollowEnabledRef.current = endFollowEnabled; | 401 | endFollowEnabledRef.current = endFollowEnabled; |
| 378 | const userInputRepinTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | 402 | const overlayRepinTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 403 | const previousWorkingControlStateRef = useRef({ | ||
| 404 | threadKey: selectedThreadKey, | ||
| 405 | visible: false, | ||
| 406 | }); | ||
| 379 | // The list's own corrections for these inset changes drift on short | 407 | // The list's own corrections for these inset changes drift on short |
| 380 | // content (and the error compounds across toggles), so deterministically | 408 | // content (and the error compounds across toggles), so deterministically |
| 381 | // re-pin the end once a toggle settles: a no-op when the resting position | 409 | // re-pin the end once a toggle settles: a no-op when the resting position |
| 382 | // is already right, corrective when it is not. Follow state is re-checked | 410 | // is already right, corrective when it is not. Follow state is re-checked |
| 383 | // inside the callback — the user may grab the list during the settle | 411 | // inside the callback — the user may grab the list during the settle |
| 384 | // window, and yanking them back would override a live gesture. | 412 | // window, and yanking them back would override a live gesture. |
| 385 | const scheduleUserInputRepin = useCallback( | 413 | const scheduleOverlayRepin = useCallback( |
| 386 | (delayMs: number) => { | 414 | (delayMs: number) => { |
| 387 | if (userInputRepinTimerRef.current !== null) { | 415 | if (overlayRepinTimerRef.current !== null) { |
| 388 | clearTimeout(userInputRepinTimerRef.current); | 416 | clearTimeout(overlayRepinTimerRef.current); |
| 389 | } | 417 | } |
| 390 | userInputRepinTimerRef.current = setTimeout(() => { | 418 | overlayRepinTimerRef.current = setTimeout(() => { |
| 391 | userInputRepinTimerRef.current = null; | 419 | overlayRepinTimerRef.current = null; |
| 392 | if (!endFollowEnabledRef.current) { | 420 | if (!endFollowEnabledRef.current) { |
| 393 | return; | 421 | return; |
| 394 | } | 422 | } |
| ... | @@ -401,12 +429,29 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -401,12 +429,29 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 401 | ); | 429 | ); |
| 402 | useEffect( | 430 | useEffect( |
| 403 | () => () => { | 431 | () => () => { |
| 404 | if (userInputRepinTimerRef.current !== null) { | 432 | if (overlayRepinTimerRef.current !== null) { |
| 405 | clearTimeout(userInputRepinTimerRef.current); | 433 | clearTimeout(overlayRepinTimerRef.current); |
| 406 | } | 434 | } |
| 407 | }, | 435 | }, |
| 408 | [], | 436 | [], |
| 409 | ); | 437 | ); |
| 438 | useEffect(() => { | ||
| 439 | const previous = previousWorkingControlStateRef.current; | ||
| 440 | const threadChanged = previous.threadKey !== selectedThreadKey; | ||
| 441 | const visibilityChanged = previous.visible !== showWorkingControl; | ||
| 442 | previousWorkingControlStateRef.current = { | ||
| 443 | threadKey: selectedThreadKey, | ||
| 444 | visible: showWorkingControl, | ||
| 445 | }; | ||
| 446 | if ((!threadChanged && !visibilityChanged) || (threadChanged && !showWorkingControl)) { | ||
| 447 | return; | ||
| 448 | } | ||
| 449 | // LegendList applies the larger inset but does not re-anchor short | ||
| 450 | // followed conversations when this floating coverage changes after the | ||
| 451 | // initial load. Re-pin after the finite inset transition; the callback | ||
| 452 | // checks follow state again so a user who scrolled up stays put. | ||
| 453 | scheduleOverlayRepin(230); | ||
| 454 | }, [scheduleOverlayRepin, selectedThreadKey, showWorkingControl]); | ||
| 410 | const handleToggleUserInputCollapsed = useCallback(() => { | 455 | const handleToggleUserInputCollapsed = useCallback(() => { |
| 411 | if (activeUserInputRequestId === null) { | 456 | if (activeUserInputRequestId === null) { |
| 412 | return; | 457 | return; |
| ... | @@ -416,7 +461,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -416,7 +461,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 416 | userInputCardProgress.value = withTiming(1, USER_INPUT_TOGGLE_TIMING); | 461 | userInputCardProgress.value = withTiming(1, USER_INPUT_TOGGLE_TIMING); |
| 417 | userInputInsetProgress.value = withTiming(1, USER_INPUT_TOGGLE_TIMING); | 462 | userInputInsetProgress.value = withTiming(1, USER_INPUT_TOGGLE_TIMING); |
| 418 | setCollapsedUserInputRequestId(null); | 463 | setCollapsedUserInputRequestId(null); |
| 419 | scheduleUserInputRepin(USER_INPUT_TOGGLE_DURATION_MS + 50); | 464 | scheduleOverlayRepin(USER_INPUT_TOGGLE_DURATION_MS + 50); |
| 420 | } else { | 465 | } else { |
| 421 | // Collapsing hides the custom-answer inputs; release the keyboard with | 466 | // Collapsing hides the custom-answer inputs; release the keyboard with |
| 422 | // them instead of leaving it up over a dead responder. | 467 | // them instead of leaving it up over a dead responder. |
| ... | @@ -427,11 +472,11 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -427,11 +472,11 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 427 | // anchor. | 472 | // anchor. |
| 428 | userInputInsetProgress.value = 0; | 473 | userInputInsetProgress.value = 0; |
| 429 | setCollapsedUserInputRequestId(activeUserInputRequestId); | 474 | setCollapsedUserInputRequestId(activeUserInputRequestId); |
| 430 | scheduleUserInputRepin(60); | 475 | scheduleOverlayRepin(60); |
| 431 | } | 476 | } |
| 432 | }, [ | 477 | }, [ |
| 433 | activeUserInputRequestId, | 478 | activeUserInputRequestId, |
| 434 | scheduleUserInputRepin, | 479 | scheduleOverlayRepin, |
| 435 | userInputCardProgress, | 480 | userInputCardProgress, |
| 436 | userInputCollapsed, | 481 | userInputCollapsed, |
| 437 | userInputInsetProgress, | 482 | userInputInsetProgress, |
| ... | @@ -622,7 +667,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -622,7 +667,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 622 | submittedMessageId={submittedMessageId} | 667 | submittedMessageId={submittedMessageId} |
| 623 | contentInsetEndAdjustment={combinedContentInsetEndAdjustment} | 668 | contentInsetEndAdjustment={combinedContentInsetEndAdjustment} |
| 624 | contentTopInset={0} | 669 | contentTopInset={0} |
| 625 | contentBottomInset={estimatedOverlayHeight} | 670 | contentBottomInset={ |
| 671 | estimatedOverlayHeight + (showWorkingControl ? FLOATING_WORKING_CONTROL_COVERAGE : 0) | ||
| 672 | } | ||
| 626 | contentMaxWidth={contentMaxWidth} | 673 | contentMaxWidth={contentMaxWidth} |
| 627 | layoutVariant={layoutVariant} | 674 | layoutVariant={layoutVariant} |
| 628 | usesAutomaticContentInsets={props.usesAutomaticContentInsets} | 675 | usesAutomaticContentInsets={props.usesAutomaticContentInsets} |
| ... | @@ -639,137 +686,110 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread | ... | @@ -639,137 +686,110 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread |
| 639 | {/* Floating composer — sticks to keyboard via KeyboardStickyView */} | 686 | {/* Floating composer — sticks to keyboard via KeyboardStickyView */} |
| 640 | {showContent ? ( | 687 | {showContent ? ( |
| 641 | <KeyboardStickyView | 688 | <KeyboardStickyView |
| 642 | // The animated keyboard height can remain stale after a dismissed | 689 | // iOS emits a native animated height target on both will-show and |
| 643 | // IME on both platforms. Visibility is the authoritative closed | 690 | // will-hide, so stay subscribed for the full transition. Android |
| 644 | // state, so disable the translation rather than stranding the pill. | 691 | // retains its background/resume stale-state quarantine. |
| 645 | enabled={isKeyboardVisible && !keyboardStateSuspect} | 692 | enabled={Platform.OS === "ios" || (isKeyboardVisible && !keyboardStateSuspect)} |
| 646 | style={{ position: "absolute", bottom: 0, left: 0, right: 0 }} | 693 | pointerEvents="box-none" |
| 694 | style={{ position: "absolute", bottom: 0, left: 0, right: 0, top: 0 }} | ||
| 647 | offset={{ closed: 0, opened: 0 }} | 695 | offset={{ closed: 0, opened: 0 }} |
| 648 | > | 696 | > |
| 649 | {/* No paddingTop here: the overlay's measured height becomes the | 697 | {/* The fixed sticky host gives this bottom-anchored child a stable |
| 650 | list's bottom inset, so any padding above the pill/composer | 698 | coordinate space. Its top and height can then animate together |
| 651 | pushes the resting content floor up by the same amount. */} | 699 | instead of the auto-sized host jumping to Yoga's destination. */} |
| 652 | <View ref={composerOverlayRef} onLayout={onComposerLayout} className="w-full"> | 700 | <Animated.View |
| 653 | {showScrollToEndButton ? ( | 701 | layout={COMPOSER_LAYOUT_TRANSITION} |
| 654 | <Animated.View | 702 | pointerEvents="box-none" |
| 655 | pointerEvents="box-none" | 703 | style={{ position: "absolute", bottom: 0, left: 0, right: 0 }} |
| 656 | className="absolute -top-11 left-0 right-0 z-20 items-center" | 704 | > |
| 657 | entering={FadeInDown.duration(160)} | 705 | {/* No paddingTop here: the overlay's measured height becomes the |
| 658 | exiting={FadeOut.duration(100)} | 706 | list's bottom inset, so any padding above the pill/composer |
| 659 | > | 707 | pushes the resting content floor up by the same amount. */} |
| 660 | {isLiquidGlassSupported ? ( | 708 | <View ref={composerOverlayRef} onLayout={onComposerLayout} className="w-full"> |
| 661 | <LiquidGlassView | 709 | <FloatingWorkingControl |
| 662 | colorScheme={isDarkMode ? "dark" : "light"} | 710 | colorScheme={isDarkMode ? "dark" : "light"} |
| 663 | effect="regular" | 711 | startedAt={floatingWorkingStartedAt} |
| 664 | interactive | 712 | showScrollToEnd={showScrollToEndButton} |
| 665 | // Interactive glass can render larger than the requested | 713 | onScrollToEnd={handleScrollToEnd} |
| 666 | // box (minimum touch size), so center the pill instead of | 714 | /> |
| 667 | // relying on it filling the glass exactly. | 715 | <View className="w-full self-center" style={{ maxWidth: contentMaxWidth }}> |
| 668 | style={{ | 716 | {props.activePendingApproval || props.activePendingUserInput ? ( |
| 669 | alignItems: "center", | 717 | <Animated.View |
| 670 | borderRadius: 18, | 718 | className="shrink-0 gap-3 px-4 pb-3" |
| 671 | height: 36, | 719 | // The questionnaire replaces the composer, so it must pad |
| 672 | justifyContent: "center", | 720 | // the home indicator the composer normally covers. |
| 673 | overflow: "hidden", | 721 | style={ |
| 674 | width: 36, | 722 | activeUserInputRequestId !== null |
| 675 | }} | 723 | ? { paddingBottom: composerBottomInset } |
| 724 | : undefined | ||
| 725 | } | ||
| 726 | entering={FadeInDown.duration(220)} | ||
| 727 | exiting={FadeOut.duration(140)} | ||
| 676 | > | 728 | > |
| 677 | <ControlPill | 729 | {props.activePendingApproval ? ( |
| 678 | accessibilityLabel="Scroll to end" | 730 | <PendingApprovalCard |
| 679 | activateOnPressIn | 731 | approval={props.activePendingApproval} |
| 680 | className="h-9 w-9 bg-transparent" | 732 | respondingApprovalId={props.respondingApprovalId} |
| 681 | icon={{ ios: "chevron.down", android: "keyboard_arrow_down" }} | 733 | onRespond={props.onRespondToApproval} |
| 682 | onPress={handleScrollToEnd} | 734 | /> |
| 683 | /> | 735 | ) : null} |
| 684 | </LiquidGlassView> | 736 | {props.activePendingUserInput ? ( |
| 685 | ) : ( | 737 | <PendingUserInputCard |
| 686 | <ControlPill | 738 | pendingUserInput={props.activePendingUserInput} |
| 687 | accessibilityLabel="Scroll to end" | 739 | maxHeight={pendingUserInputMaxHeight} |
| 688 | activateOnPressIn | 740 | collapsed={userInputCollapsed} |
| 689 | className="h-9 w-9 border border-border bg-card shadow-md shadow-black/10" | 741 | onToggleCollapsed={handleToggleUserInputCollapsed} |
| 690 | icon={{ ios: "chevron.down", android: "keyboard_arrow_down" }} | 742 | onStopThread={props.onStopThread} |
| 691 | onPress={handleScrollToEnd} | 743 | cardProgress={userInputCardProgress} |
| 692 | /> | 744 | cardCoverage={userInputCardCoverage} |
| 693 | )} | 745 | onInputFocusChange={handleOwnedInputFocusChange} |
| 694 | </Animated.View> | 746 | drafts={props.activePendingUserInputDrafts} |
| 695 | ) : null} | 747 | answers={props.activePendingUserInputAnswers} |
| 696 | <View className="w-full self-center" style={{ maxWidth: contentMaxWidth }}> | 748 | respondingUserInputId={props.respondingUserInputId} |
| 697 | {props.activePendingApproval || props.activePendingUserInput ? ( | 749 | onSelectOption={props.onSelectUserInputOption} |
| 698 | <Animated.View | 750 | onChangeCustomAnswer={props.onChangeUserInputCustomAnswer} |
| 699 | className="shrink-0 gap-3 px-4 pb-3" | 751 | onSubmit={props.onSubmitUserInput} |
| 700 | // The questionnaire replaces the composer, so it must pad | 752 | /> |
| 701 | // the home indicator the composer normally covers. | 753 | ) : null} |
| 702 | style={ | 754 | </Animated.View> |
| 703 | activeUserInputRequestId !== null | 755 | ) : null} |
| 704 | ? { paddingBottom: composerBottomInset } | 756 | </View> |
| 705 | : undefined | 757 | |
| 706 | } | 758 | {/* Hidden (not unmounted) while a user-input request owns the |
| 707 | entering={FadeInDown.duration(220)} | ||
| 708 | exiting={FadeOut.duration(140)} | ||
| 709 | > | ||
| 710 | {props.activePendingApproval ? ( | ||
| 711 | <PendingApprovalCard | ||
| 712 | approval={props.activePendingApproval} | ||
| 713 | respondingApprovalId={props.respondingApprovalId} | ||
| 714 | onRespond={props.onRespondToApproval} | ||
| 715 | /> | ||
| 716 | ) : null} | ||
| 717 | {props.activePendingUserInput ? ( | ||
| 718 | <PendingUserInputCard | ||
| 719 | pendingUserInput={props.activePendingUserInput} | ||
| 720 | maxHeight={pendingUserInputMaxHeight} | ||
| 721 | collapsed={userInputCollapsed} | ||
| 722 | onToggleCollapsed={handleToggleUserInputCollapsed} | ||
| 723 | onStopThread={props.onStopThread} | ||
| 724 | cardProgress={userInputCardProgress} | ||
| 725 | cardCoverage={userInputCardCoverage} | ||
| 726 | onInputFocusChange={handleOwnedInputFocusChange} | ||
| 727 | drafts={props.activePendingUserInputDrafts} | ||
| 728 | answers={props.activePendingUserInputAnswers} | ||
| 729 | respondingUserInputId={props.respondingUserInputId} | ||
| 730 | onSelectOption={props.onSelectUserInputOption} | ||
| 731 | onChangeCustomAnswer={props.onChangeUserInputCustomAnswer} | ||
| 732 | onSubmit={props.onSubmitUserInput} | ||
| 733 | /> | ||
| 734 | ) : null} | ||
| 735 | </Animated.View> | ||
| 736 | ) : null} | ||
| 737 | </View> | ||
| 738 | |||
| 739 | {/* Hidden (not unmounted) while a user-input request owns the | ||
| 740 | composer slot, so composer drafts and editor state survive. */} | 759 | composer slot, so composer drafts and editor state survive. */} |
| 741 | <View style={activeUserInputRequestId !== null ? { display: "none" } : undefined}> | 760 | <View style={activeUserInputRequestId !== null ? { display: "none" } : undefined}> |
| 742 | <ThreadComposer | 761 | <ThreadComposer |
| 743 | editorRef={composerEditorRef} | 762 | editorRef={composerEditorRef} |
| 744 | draftMessage={props.draftMessage} | 763 | draftMessage={props.draftMessage} |
| 745 | draftAttachments={props.draftAttachments} | 764 | draftAttachments={props.draftAttachments} |
| 746 | placeholder="Ask the repo agent, or run a command…" | 765 | placeholder="Ask the repo agent, or run a command…" |
| 747 | contentMaxWidth={contentMaxWidth} | 766 | contentMaxWidth={contentMaxWidth} |
| 748 | connectionState={props.connectionStateLabel} | 767 | connectionState={props.connectionStateLabel} |
| 749 | connectionError={props.connectionError} | 768 | connectionError={props.connectionError} |
| 750 | environmentLabel={props.environmentLabel} | 769 | environmentLabel={props.environmentLabel} |
| 751 | threadSyncPhase={threadSyncPhase} | 770 | threadSyncPhase={threadSyncPhase} |
| 752 | selectedThread={props.selectedThread} | 771 | selectedThread={props.selectedThread} |
| 753 | serverConfig={props.serverConfig} | 772 | serverConfig={props.serverConfig} |
| 754 | queueCount={props.selectedThreadQueueCount} | 773 | queueCount={props.selectedThreadQueueCount} |
| 755 | environmentId={props.environmentId} | 774 | environmentId={props.environmentId} |
| 756 | projectCwd={props.projectWorkspaceRoot} | 775 | projectCwd={props.projectWorkspaceRoot} |
| 757 | bottomInset={composerBottomInset} | 776 | bottomInset={composerBottomInset} |
| 758 | onChangeDraftMessage={props.onChangeDraftMessage} | 777 | onChangeDraftMessage={props.onChangeDraftMessage} |
| 759 | onPickDraftImages={props.onPickDraftImages} | 778 | onPickDraftImages={props.onPickDraftImages} |
| 760 | onNativePasteImages={props.onNativePasteImages} | 779 | onNativePasteImages={props.onNativePasteImages} |
| 761 | onRemoveDraftImage={props.onRemoveDraftImage} | 780 | onRemoveDraftImage={props.onRemoveDraftImage} |
| 762 | onStopThread={props.onStopThread} | 781 | onStopThread={props.onStopThread} |
| 763 | onSendMessage={handleSendMessage} | 782 | onSendMessage={handleSendMessage} |
| 764 | onReconnectEnvironment={props.onReconnectEnvironment} | 783 | onReconnectEnvironment={props.onReconnectEnvironment} |
| 765 | onUpdateModelSelection={props.onUpdateThreadModelSelection} | 784 | onUpdateModelSelection={props.onUpdateThreadModelSelection} |
| 766 | onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode} | 785 | onUpdateRuntimeMode={props.onUpdateThreadRuntimeMode} |
| 767 | onUpdateInteractionMode={props.onUpdateThreadInteractionMode} | 786 | onUpdateInteractionMode={props.onUpdateThreadInteractionMode} |
| 768 | onExpandedChange={setComposerExpanded} | 787 | onExpandedChange={setComposerExpanded} |
| 769 | onEditorFocusChange={handleOwnedInputFocusChange} | 788 | onEditorFocusChange={handleOwnedInputFocusChange} |
| 770 | /> | 789 | /> |
| 790 | </View> | ||
| 771 | </View> | 791 | </View> |
| 772 | </View> | 792 | </Animated.View> |
| 773 | </KeyboardStickyView> | 793 | </KeyboardStickyView> |
| 774 | ) : null} | 794 | ) : null} |
| 775 | </View> | 795 | </View> |
apps/mobile/src/features/threads/ThreadFeed.tsx+122-95| ... | @@ -4,7 +4,6 @@ import { type LegendListRef } from "@legendapp/list/react-native"; | ... | @@ -4,7 +4,6 @@ import { type LegendListRef } from "@legendapp/list/react-native"; |
| 4 | import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts"; | 4 | import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts"; |
| 5 | import { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images"; | 5 | import { classifyMarkdownImageSource } from "@t3tools/client-runtime/markdown-images"; |
| 6 | import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; | 6 | import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; |
| 7 | import { formatElapsed } from "@t3tools/shared/orchestrationTiming"; | ||
| 8 | import { SymbolView } from "../../components/AppSymbol"; | 7 | import { SymbolView } from "../../components/AppSymbol"; |
| 9 | import { HeaderHeightContext } from "@react-navigation/elements"; | 8 | import { HeaderHeightContext } from "@react-navigation/elements"; |
| 10 | import { useNavigation } from "@react-navigation/native"; | 9 | import { useNavigation } from "@react-navigation/native"; |
| ... | @@ -45,7 +44,13 @@ import { | ... | @@ -45,7 +44,13 @@ import { |
| 45 | import { TouchableOpacity } from "react-native-gesture-handler"; | 44 | import { TouchableOpacity } from "react-native-gesture-handler"; |
| 46 | import ImageViewing from "react-native-image-viewing"; | 45 | import ImageViewing from "react-native-image-viewing"; |
| 47 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 46 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 48 | import Animated, { FadeIn, FadeInUp, type SharedValue } from "react-native-reanimated"; | 47 | import Animated, { |
| 48 | FadeIn, | ||
| 49 | FadeInUp, | ||
| 50 | FadeOut, | ||
| 51 | LinearTransition, | ||
| 52 | type SharedValue, | ||
| 53 | } from "react-native-reanimated"; | ||
| 49 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; | 54 | import { useUniwindTheme } from "../../lib/useUniwindTheme"; |
| 50 | import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics"; | 55 | import { IOS_NAV_BAR_HEIGHT } from "../../lib/layoutMetrics"; |
| 51 | import { useFontFamily } from "../../lib/useFontFamily"; | 56 | import { useFontFamily } from "../../lib/useFontFamily"; |
| ... | @@ -84,9 +89,7 @@ import { | ... | @@ -84,9 +89,7 @@ import { |
| 84 | import { | 89 | import { |
| 85 | resolveMarkdownFontSizes, | 90 | resolveMarkdownFontSizes, |
| 86 | resolveNativeMarkdownTypography, | 91 | resolveNativeMarkdownTypography, |
| 87 | scaledTypographyLineHeight, | ||
| 88 | } from "../../lib/appearancePreferences"; | 92 | } from "../../lib/appearancePreferences"; |
| 89 | import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; | ||
| 90 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | 93 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; |
| 91 | import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; | 94 | import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; |
| 92 | import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; | 95 | import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; |
| ... | @@ -105,6 +108,7 @@ import { | ... | @@ -105,6 +108,7 @@ import { |
| 105 | collapsedWorkLogHeight, | 108 | collapsedWorkLogHeight, |
| 106 | ThreadWorkGroupToggle, | 109 | ThreadWorkGroupToggle, |
| 107 | ThreadWorkLog, | 110 | ThreadWorkLog, |
| 111 | THREAD_DISCLOSURE_TRANSITION_MS, | ||
| 108 | WORK_GROUP_TOGGLE_HEIGHT, | 112 | WORK_GROUP_TOGGLE_HEIGHT, |
| 109 | } from "./thread-work-log"; | 113 | } from "./thread-work-log"; |
| 110 | import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; | 114 | import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; |
| ... | @@ -134,9 +138,10 @@ function formatMessageTime(input: string): string { | ... | @@ -134,9 +138,10 @@ function formatMessageTime(input: string): string { |
| 134 | // so its height is a constant; a drifted value costs one correction on | 138 | // so its height is a constant; a drifted value costs one correction on |
| 135 | // measure, not a persistent offset. | 139 | // measure, not a persistent offset. |
| 136 | const TURN_FOLD_HEIGHT = 56; // min-h-11 (44) + mb-3 (12) | 140 | const TURN_FOLD_HEIGHT = 56; // min-h-11 (44) + mb-3 (12) |
| 137 | // The working row has no min-height clamp — its height follows the scaled | 141 | const THREAD_FEED_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_TRANSITION_MS); |
| 138 | // text-xs line height (see workingRowHeight in ThreadFeed). | 142 | const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.duration(140); |
| 139 | const WORKING_ROW_VERTICAL_EXTRAS = 24; // py-1 (8) + mb-4 (16) | 143 | const THREAD_FEED_DISCLOSURE_EXIT_TRANSITION = FadeOut.duration(120); |
| 144 | const EMPTY_DISCLOSURE_ENTRY_IDS: ReadonlySet<string> = new Set(); | ||
| 140 | 145 | ||
| 141 | // Entering animations must only play for rows born just now — LegendList | 146 | // Entering animations must only play for rows born just now — LegendList |
| 142 | // remounts rows when they scroll back into view, and replaying an entrance for | 147 | // remounts rows when they scroll back into view, and replaying an entrance for |
| ... | @@ -985,10 +990,6 @@ function renderFeedEntry( | ... | @@ -985,10 +990,6 @@ function renderFeedEntry( |
| 985 | const entry = info.item; | 990 | const entry = info.item; |
| 986 | const { markdownStyles, iconSubtleColor, userBubbleColor } = props; | 991 | const { markdownStyles, iconSubtleColor, userBubbleColor } = props; |
| 987 | 992 | ||
| 988 | if (entry.type === "working") { | ||
| 989 | return <WorkingTimelineRow startedAt={entry.createdAt} />; | ||
| 990 | } | ||
| 991 | |||
| 992 | if (entry.type === "turn-fold") { | 993 | if (entry.type === "turn-fold") { |
| 993 | return ( | 994 | return ( |
| 994 | <Pressable | 995 | <Pressable |
| ... | @@ -1017,7 +1018,10 @@ function renderFeedEntry( | ... | @@ -1017,7 +1018,10 @@ function renderFeedEntry( |
| 1017 | expanded={entry.expanded} | 1018 | expanded={entry.expanded} |
| 1018 | hiddenCount={entry.hiddenCount} | 1019 | hiddenCount={entry.hiddenCount} |
| 1019 | iconSubtleColor={iconSubtleColor} | 1020 | iconSubtleColor={iconSubtleColor} |
| 1020 | onlyToolActivities={entry.onlyToolActivities} | 1021 | summary={entry.summary} |
| 1022 | summaryKind={entry.summaryKind} | ||
| 1023 | hasFailure={entry.hasFailure} | ||
| 1024 | shimmer={entry.shimmer} | ||
| 1021 | onToggle={() => props.onToggleWorkGroup(entry.groupId)} | 1025 | onToggle={() => props.onToggleWorkGroup(entry.groupId)} |
| 1022 | /> | 1026 | /> |
| 1023 | ); | 1027 | ); |
| ... | @@ -1180,32 +1184,6 @@ function renderFeedEntry( | ... | @@ -1180,32 +1184,6 @@ function renderFeedEntry( |
| 1180 | ); | 1184 | ); |
| 1181 | } | 1185 | } |
| 1182 | 1186 | ||
| 1183 | const WorkingTimelineRow = memo(function WorkingTimelineRow(props: { readonly startedAt: string }) { | ||
| 1184 | const [nowMs, setNowMs] = useState(() => Date.now()); | ||
| 1185 | |||
| 1186 | useEffect(() => { | ||
| 1187 | const intervalId = setInterval(() => { | ||
| 1188 | setNowMs(Date.now()); | ||
| 1189 | }, 1_000); | ||
| 1190 | return () => clearInterval(intervalId); | ||
| 1191 | }, [props.startedAt]); | ||
| 1192 | |||
| 1193 | const durationLabel = formatElapsed(props.startedAt, new Date(nowMs).toISOString()) ?? "0s"; | ||
| 1194 | |||
| 1195 | return ( | ||
| 1196 | <View className="mb-4 flex-row items-center gap-2 px-1.5 py-1"> | ||
| 1197 | <View className="flex-row items-center gap-1"> | ||
| 1198 | <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-500" /> | ||
| 1199 | <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a80-500-a80" /> | ||
| 1200 | <View className="h-1 w-1 rounded-full bg-adaptive-neutral-400-a60-500-a60" /> | ||
| 1201 | </View> | ||
| 1202 | <Text className="font-t3-medium text-xs tabular-nums text-adaptive-neutral-600-400"> | ||
| 1203 | Working for {durationLabel} | ||
| 1204 | </Text> | ||
| 1205 | </View> | ||
| 1206 | ); | ||
| 1207 | }); | ||
| 1208 | |||
| 1209 | function UserMessageContent(props: { | 1187 | function UserMessageContent(props: { |
| 1210 | readonly text: string; | 1188 | readonly text: string; |
| 1211 | readonly markdownStyles: MarkdownStyleSet; | 1189 | readonly markdownStyles: MarkdownStyleSet; |
| ... | @@ -1479,14 +1457,14 @@ function ThreadFeedPlaceholder(props: { | ... | @@ -1479,14 +1457,14 @@ function ThreadFeedPlaceholder(props: { |
| 1479 | export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | 1457 | export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1480 | const navigation = useNavigation(); | 1458 | const navigation = useNavigation(); |
| 1481 | const copyFeedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | 1459 | const copyFeedbackTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 1482 | const foldSettleFrameRef = useRef<number | null>(null); | 1460 | const disclosureSettleFrameRef = useRef<number | null>(null); |
| 1483 | const foldSettleSecondFrameRef = useRef<number | null>(null); | 1461 | const disclosureSettleSecondFrameRef = useRef<number | null>(null); |
| 1484 | const disclosureAnchorKeyRef = useRef<string | null>(null); | 1462 | const disclosureAnchorKeyRef = useRef<string | null>(null); |
| 1463 | const previousPresentedFeedRef = useRef<ReadonlyArray<ThreadFeedEntry> | null>(null); | ||
| 1485 | const headerMaterialVisibleRef = useRef(false); | 1464 | const headerMaterialVisibleRef = useRef(false); |
| 1486 | const previousLatestTurnRef = useRef(props.latestTurn); | 1465 | const previousLatestTurnRef = useRef(props.latestTurn); |
| 1487 | const userScrollSettleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); | 1466 | const userScrollSettleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); |
| 1488 | const { width: windowWidth } = useWindowDimensions(); | 1467 | const { width: windowWidth } = useWindowDimensions(); |
| 1489 | const { appearance } = useAppearancePreferences(); | ||
| 1490 | const [viewportWidth, setViewportWidth] = useState(() => | 1468 | const [viewportWidth, setViewportWidth] = useState(() => |
| 1491 | props.layoutVariant === "split" ? 0 : windowWidth, | 1469 | props.layoutVariant === "split" ? 0 : windowWidth, |
| 1492 | ); | 1470 | ); |
| ... | @@ -1791,14 +1769,37 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1791,14 +1769,37 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1791 | props.latestTurn, | 1769 | props.latestTurn, |
| 1792 | ], | 1770 | ], |
| 1793 | ); | 1771 | ); |
| 1772 | const disclosureEnteringEntryIds = useMemo(() => { | ||
| 1773 | const anchorKey = disclosureAnchorKeyRef.current; | ||
| 1774 | const previousPresentedFeed = previousPresentedFeedRef.current; | ||
| 1775 | if (!disclosureToggleSettling || anchorKey === null || previousPresentedFeed === null) { | ||
| 1776 | return EMPTY_DISCLOSURE_ENTRY_IDS; | ||
| 1777 | } | ||
| 1778 | |||
| 1779 | const previousIds = new Set(previousPresentedFeed.map((entry) => entry.id)); | ||
| 1780 | const anchorIndex = presentedFeed.findIndex((entry) => entry.id === anchorKey); | ||
| 1781 | const enteringIds = new Set<string>(); | ||
| 1782 | if (anchorIndex < 0) { | ||
| 1783 | return enteringIds; | ||
| 1784 | } | ||
| 1785 | for (let index = anchorIndex + 1; index < presentedFeed.length; index += 1) { | ||
| 1786 | const entryId = presentedFeed[index]!.id; | ||
| 1787 | if (previousIds.has(entryId)) { | ||
| 1788 | break; | ||
| 1789 | } | ||
| 1790 | enteringIds.add(entryId); | ||
| 1791 | } | ||
| 1792 | return enteringIds; | ||
| 1793 | }, [disclosureToggleSettling, presentedFeed]); | ||
| 1794 | |||
| 1795 | useLayoutEffect(() => { | ||
| 1796 | previousPresentedFeedRef.current = presentedFeed; | ||
| 1797 | }, [presentedFeed]); | ||
| 1794 | 1798 | ||
| 1795 | // The empty↔filled key below remounts the list, which resets its imperative | 1799 | // The empty↔filled key below remounts the list and resets its imperative |
| 1796 | // content-inset override — and useKeyboardChatComposerInset (mounted above | 1800 | // content-inset override. Seed the fresh instance synchronously with the |
| 1797 | // the remount boundary) deduplicates by height, so it never re-reports the | 1801 | // current overlay height before the scroll integration's next reaction; |
| 1798 | // composer inset to the fresh instance. Re-report the measured overlay height | 1802 | // on Android the declarative contentInset floor covers this same window. |
| 1799 | // (composer plus any pending approval / user-input card) so the remounted | ||
| 1800 | // list's scroll math gets the true value; on Android the declarative | ||
| 1801 | // contentInset floor below covers the window before this effect lands. | ||
| 1802 | const listMountKey = `${feedThreadKey}:${props.feed.length === 0 ? "empty" : "filled"}`; | 1803 | const listMountKey = `${feedThreadKey}:${props.feed.length === 0 ? "empty" : "filled"}`; |
| 1803 | useLayoutEffect(() => { | 1804 | useLayoutEffect(() => { |
| 1804 | const bottom = props.contentInsetEndAdjustment.value; | 1805 | const bottom = props.contentInsetEndAdjustment.value; |
| ... | @@ -1863,34 +1864,52 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1863,34 +1864,52 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1863 | if (copyFeedbackTimeoutRef.current) { | 1864 | if (copyFeedbackTimeoutRef.current) { |
| 1864 | clearTimeout(copyFeedbackTimeoutRef.current); | 1865 | clearTimeout(copyFeedbackTimeoutRef.current); |
| 1865 | } | 1866 | } |
| 1866 | if (foldSettleFrameRef.current !== null) { | 1867 | if (disclosureSettleFrameRef.current !== null) { |
| 1867 | cancelAnimationFrame(foldSettleFrameRef.current); | 1868 | cancelAnimationFrame(disclosureSettleFrameRef.current); |
| 1868 | } | 1869 | } |
| 1869 | if (foldSettleSecondFrameRef.current !== null) { | 1870 | if (disclosureSettleSecondFrameRef.current !== null) { |
| 1870 | cancelAnimationFrame(foldSettleSecondFrameRef.current); | 1871 | cancelAnimationFrame(disclosureSettleSecondFrameRef.current); |
| 1871 | } | 1872 | } |
| 1872 | }; | 1873 | }; |
| 1873 | }, []); | 1874 | }, []); |
| 1874 | 1875 | ||
| 1875 | const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string | null) => { | 1876 | const settleDisclosureAfterLayout = useCallback(() => { |
| 1876 | disclosureAnchorKeyRef.current = anchorKey; | 1877 | if (disclosureSettleFrameRef.current !== null) { |
| 1877 | setDisclosureToggleSettling(true); | 1878 | cancelAnimationFrame(disclosureSettleFrameRef.current); |
| 1878 | if (foldSettleFrameRef.current !== null) { | ||
| 1879 | cancelAnimationFrame(foldSettleFrameRef.current); | ||
| 1880 | } | 1879 | } |
| 1881 | if (foldSettleSecondFrameRef.current !== null) { | 1880 | if (disclosureSettleSecondFrameRef.current !== null) { |
| 1882 | cancelAnimationFrame(foldSettleSecondFrameRef.current); | 1881 | cancelAnimationFrame(disclosureSettleSecondFrameRef.current); |
| 1883 | } | 1882 | } |
| 1884 | foldSettleFrameRef.current = requestAnimationFrame(() => { | 1883 | disclosureSettleFrameRef.current = requestAnimationFrame(() => { |
| 1885 | foldSettleSecondFrameRef.current = requestAnimationFrame(() => { | 1884 | disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => { |
| 1886 | disclosureAnchorKeyRef.current = null; | 1885 | disclosureAnchorKeyRef.current = null; |
| 1887 | setDisclosureToggleSettling(false); | 1886 | setDisclosureToggleSettling(false); |
| 1888 | foldSettleFrameRef.current = null; | 1887 | disclosureSettleFrameRef.current = null; |
| 1889 | foldSettleSecondFrameRef.current = null; | 1888 | disclosureSettleSecondFrameRef.current = null; |
| 1890 | }); | 1889 | }); |
| 1891 | }); | 1890 | }); |
| 1892 | }, []); | 1891 | }, []); |
| 1893 | 1892 | ||
| 1893 | const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string | null) => { | ||
| 1894 | disclosureAnchorKeyRef.current = anchorKey; | ||
| 1895 | setDisclosureToggleSettling(true); | ||
| 1896 | }, []); | ||
| 1897 | |||
| 1898 | // Start the quiet-frame countdown after React has committed the disclosure. | ||
| 1899 | // Every measured item-size change restarts it, so end maintenance cannot | ||
| 1900 | // wake between the data mutation and LegendList's final layout correction. | ||
| 1901 | useLayoutEffect(() => { | ||
| 1902 | if (disclosureAnchorKeyRef.current !== null) { | ||
| 1903 | settleDisclosureAfterLayout(); | ||
| 1904 | } | ||
| 1905 | }, [expandedTurnIds, expandedWorkGroups, expandedWorkRows, settleDisclosureAfterLayout]); | ||
| 1906 | |||
| 1907 | const handleItemSizeChanged = useCallback(() => { | ||
| 1908 | if (disclosureAnchorKeyRef.current !== null) { | ||
| 1909 | settleDisclosureAfterLayout(); | ||
| 1910 | } | ||
| 1911 | }, [settleDisclosureAfterLayout]); | ||
| 1912 | |||
| 1894 | const shouldRestoreVisibleContentPosition = useCallback((entry: ThreadFeedEntry) => { | 1913 | const shouldRestoreVisibleContentPosition = useCallback((entry: ThreadFeedEntry) => { |
| 1895 | const disclosureAnchorKey = disclosureAnchorKeyRef.current; | 1914 | const disclosureAnchorKey = disclosureAnchorKeyRef.current; |
| 1896 | return disclosureAnchorKey === null || entry.id === disclosureAnchorKey; | 1915 | return disclosureAnchorKey === null || entry.id === disclosureAnchorKey; |
| ... | @@ -1975,11 +1994,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1975,11 +1994,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1975 | // scrolling up through unmeasured content corrects each row's height as it | 1994 | // scrolling up through unmeasured content corrects each row's height as it |
| 1976 | // mounts — the feed visibly jumps. Fixed sizes make the small chrome rows | 1995 | // mounts — the feed visibly jumps. Fixed sizes make the small chrome rows |
| 1977 | // exact; message rows stay undefined and use LegendList's per-type running | 1996 | // exact; message rows stay undefined and use LegendList's per-type running |
| 1978 | // average once one of their type has been measured. Text-driven heights | 1997 | // average once one of their type has been measured. |
| 1979 | // follow the configurable base font size via scaledTypographyLineHeight. | ||
| 1980 | const workingRowHeight = | ||
| 1981 | WORKING_ROW_VERTICAL_EXTRAS + | ||
| 1982 | scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.label, appearance.baseFontSize); | ||
| 1983 | const getFixedItemSize = useCallback( | 1998 | const getFixedItemSize = useCallback( |
| 1984 | (entry: ThreadFeedEntry) => { | 1999 | (entry: ThreadFeedEntry) => { |
| 1985 | switch (entry.type) { | 2000 | switch (entry.type) { |
| ... | @@ -1987,46 +2002,56 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -1987,46 +2002,56 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 1987 | return TURN_FOLD_HEIGHT; | 2002 | return TURN_FOLD_HEIGHT; |
| 1988 | case "work-toggle": | 2003 | case "work-toggle": |
| 1989 | return WORK_GROUP_TOGGLE_HEIGHT; | 2004 | return WORK_GROUP_TOGGLE_HEIGHT; |
| 1990 | case "working": | ||
| 1991 | return workingRowHeight; | ||
| 1992 | case "activity-group": | 2005 | case "activity-group": |
| 1993 | // Expanded rows append a variable detail block — fall back to | 2006 | // Expanded rows append a variable detail block — fall back to |
| 1994 | // measurement for those groups. | 2007 | // measurement for those groups. |
| 1995 | return entry.activities.some((activity) => expandedWorkRows[activity.id]) | 2008 | return entry.activities.some((activity) => expandedWorkRows[activity.id]) |
| 1996 | ? undefined | 2009 | ? undefined |
| 1997 | : collapsedWorkLogHeight(entry.activities, appearance.baseFontSize); | 2010 | : collapsedWorkLogHeight(entry.activities); |
| 1998 | default: | 2011 | default: |
| 1999 | return undefined; | 2012 | return undefined; |
| 2000 | } | 2013 | } |
| 2001 | }, | 2014 | }, |
| 2002 | [expandedWorkRows, workingRowHeight, appearance.baseFontSize], | 2015 | [expandedWorkRows], |
| 2003 | ); | 2016 | ); |
| 2004 | 2017 | ||
| 2005 | const renderItem = useCallback( | 2018 | const renderItem = useCallback( |
| 2006 | (info: { item: ThreadFeedEntry; index: number }) => | 2019 | (info: { item: ThreadFeedEntry; index: number }) => ( |
| 2007 | renderFeedEntry(info, { | 2020 | <Animated.View |
| 2008 | environmentId: props.environmentId, | 2021 | key={info.item.id} |
| 2009 | copiedRowId, | 2022 | entering={ |
| 2010 | expandedWorkRows, | 2023 | disclosureEnteringEntryIds.has(info.item.id) |
| 2011 | terminalAssistantMessageIds, | 2024 | ? THREAD_FEED_DISCLOSURE_ENTER_TRANSITION |
| 2012 | unsettledTurnId, | 2025 | : undefined |
| 2013 | onCopyWorkRow, | 2026 | } |
| 2014 | onToggleWorkGroup, | 2027 | exiting={THREAD_FEED_DISCLOSURE_EXIT_TRANSITION} |
| 2015 | onToggleWorkRow, | 2028 | > |
| 2016 | onToggleTurnFold, | 2029 | {renderFeedEntry(info, { |
| 2017 | onPressImage, | 2030 | environmentId: props.environmentId, |
| 2018 | onMarkdownLinkPress, | 2031 | copiedRowId, |
| 2019 | renderMarkdownImage, | 2032 | expandedWorkRows, |
| 2020 | iconSubtleColor, | 2033 | terminalAssistantMessageIds, |
| 2021 | userBubbleColor, | 2034 | unsettledTurnId, |
| 2022 | markdownStyles, | 2035 | onCopyWorkRow, |
| 2023 | reviewCommentColors, | 2036 | onToggleWorkGroup, |
| 2024 | reviewCommentBubbleWidth, | 2037 | onToggleWorkRow, |
| 2025 | userBubbleMaxWidth, | 2038 | onToggleTurnFold, |
| 2026 | skills: props.skills, | 2039 | onPressImage, |
| 2027 | }), | 2040 | onMarkdownLinkPress, |
| 2041 | renderMarkdownImage, | ||
| 2042 | iconSubtleColor, | ||
| 2043 | userBubbleColor, | ||
| 2044 | markdownStyles, | ||
| 2045 | reviewCommentColors, | ||
| 2046 | reviewCommentBubbleWidth, | ||
| 2047 | userBubbleMaxWidth, | ||
| 2048 | skills: props.skills, | ||
| 2049 | })} | ||
| 2050 | </Animated.View> | ||
| 2051 | ), | ||
| 2028 | [ | 2052 | [ |
| 2029 | copiedRowId, | 2053 | copiedRowId, |
| 2054 | disclosureEnteringEntryIds, | ||
| 2030 | expandedWorkRows, | 2055 | expandedWorkRows, |
| 2031 | terminalAssistantMessageIds, | 2056 | terminalAssistantMessageIds, |
| 2032 | unsettledTurnId, | 2057 | unsettledTurnId, |
| ... | @@ -2093,7 +2118,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -2093,7 +2118,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 2093 | } | 2118 | } |
| 2094 | : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} | 2119 | : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} |
| 2095 | {...(anchoredEndSpace ? { anchoredEndSpace } : {})} | 2120 | {...(anchoredEndSpace ? { anchoredEndSpace } : {})} |
| 2096 | // Patched LegendList prop (patches/@legendapp__list@3.2.0.patch): | 2121 | // Patched LegendList prop (patches/@legendapp__list@3.3.5.patch): |
| 2097 | // lets its scroll math clamp programmatic scrolls to -headerInset | 2122 | // lets its scroll math clamp programmatic scrolls to -headerInset |
| 2098 | // instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short | 2123 | // instead of 0, so initialScrollAtEnd/maintainScrollAtEnd on short |
| 2099 | // content rest below the transparent header rather than at frame top. | 2124 | // content rest below the transparent header rather than at frame top. |
| ... | @@ -2148,6 +2173,8 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { | ... | @@ -2148,6 +2173,8 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { |
| 2148 | entry.type === "message" ? `message:${entry.message.role}` : entry.type | 2173 | entry.type === "message" ? `message:${entry.message.role}` : entry.type |
| 2149 | } | 2174 | } |
| 2150 | getFixedItemSize={getFixedItemSize} | 2175 | getFixedItemSize={getFixedItemSize} |
| 2176 | itemLayoutAnimation={THREAD_FEED_LAYOUT_TRANSITION} | ||
| 2177 | onItemSizeChanged={handleItemSizeChanged} | ||
| 2151 | // Measure rows well before they scroll into view so estimate→actual | 2178 | // Measure rows well before they scroll into view so estimate→actual |
| 2152 | // corrections land offscreen instead of under the user's finger. | 2179 | // corrections land offscreen instead of under the user's finger. |
| 2153 | drawDistance={500} | 2180 | drawDistance={500} |
apps/mobile/src/features/threads/floating-working-control.tsx created+205| ... | @@ -0,0 +1,205 @@ | ||
| 1 | import { | ||
| 2 | isLiquidGlassSupported, | ||
| 3 | LiquidGlassContainerView, | ||
| 4 | LiquidGlassView, | ||
| 5 | } from "@callstack/liquid-glass"; | ||
| 6 | import { useEffect, useState } from "react"; | ||
| 7 | import { Text as SystemText, View } from "react-native"; | ||
| 8 | import Animated, { | ||
| 9 | Easing, | ||
| 10 | FadeIn, | ||
| 11 | FadeOut, | ||
| 12 | ReduceMotion, | ||
| 13 | useAnimatedStyle, | ||
| 14 | useSharedValue, | ||
| 15 | withTiming, | ||
| 16 | } from "react-native-reanimated"; | ||
| 17 | import { withUniwind } from "uniwind"; | ||
| 18 | |||
| 19 | import { AppText as Text } from "../../components/AppText"; | ||
| 20 | import { ControlPill } from "../../components/ControlPill"; | ||
| 21 | |||
| 22 | const CONTROL_HEIGHT = 44; | ||
| 23 | const CONTROL_COMPOSER_GAP = 8; | ||
| 24 | const GLASS_MERGE_SPACING = 12; | ||
| 25 | const CONTROL_ENTERING = FadeIn.duration(180).reduceMotion(ReduceMotion.System); | ||
| 26 | const CONTROL_EXITING = FadeOut.duration(120).reduceMotion(ReduceMotion.System); | ||
| 27 | const CONTROL_TIMING = { | ||
| 28 | duration: 240, | ||
| 29 | easing: Easing.out(Easing.cubic), | ||
| 30 | reduceMotion: ReduceMotion.System, | ||
| 31 | } as const; | ||
| 32 | const CONTROL_SEPARATION = (16 + CONTROL_HEIGHT) / 2; | ||
| 33 | |||
| 34 | const UniwindLiquidGlassView = withUniwind(LiquidGlassView); | ||
| 35 | const UniwindLiquidGlassContainerView = withUniwind(LiquidGlassContainerView); | ||
| 36 | const AnimatedLiquidGlassView = Animated.createAnimatedComponent(UniwindLiquidGlassView); | ||
| 37 | |||
| 38 | export const FLOATING_WORKING_CONTROL_COVERAGE = CONTROL_HEIGHT + CONTROL_COMPOSER_GAP; | ||
| 39 | |||
| 40 | export function FloatingWorkingControl(props: { | ||
| 41 | readonly colorScheme: "light" | "dark"; | ||
| 42 | readonly startedAt: string | null; | ||
| 43 | readonly showScrollToEnd: boolean; | ||
| 44 | readonly onScrollToEnd: () => void; | ||
| 45 | }) { | ||
| 46 | const separationProgress = useSharedValue(props.showScrollToEnd ? 1 : 0); | ||
| 47 | |||
| 48 | useEffect(() => { | ||
| 49 | separationProgress.value = withTiming(props.showScrollToEnd ? 1 : 0, CONTROL_TIMING); | ||
| 50 | }, [props.showScrollToEnd, separationProgress]); | ||
| 51 | |||
| 52 | const timerStyle = useAnimatedStyle(() => ({ | ||
| 53 | transform: [{ translateX: CONTROL_SEPARATION * (1 - separationProgress.value) }], | ||
| 54 | })); | ||
| 55 | const arrowTransformStyle = useAnimatedStyle(() => ({ | ||
| 56 | transform: [{ translateX: -CONTROL_SEPARATION * (1 - separationProgress.value) }], | ||
| 57 | })); | ||
| 58 | const arrowContentStyle = useAnimatedStyle(() => ({ | ||
| 59 | opacity: separationProgress.value, | ||
| 60 | })); | ||
| 61 | |||
| 62 | if (props.startedAt === null && !props.showScrollToEnd) { | ||
| 63 | return null; | ||
| 64 | } | ||
| 65 | |||
| 66 | return ( | ||
| 67 | <Animated.View | ||
| 68 | pointerEvents="box-none" | ||
| 69 | className="absolute left-0 right-0 z-20 items-center" | ||
| 70 | style={{ top: -FLOATING_WORKING_CONTROL_COVERAGE }} | ||
| 71 | entering={isLiquidGlassSupported ? undefined : CONTROL_ENTERING} | ||
| 72 | exiting={isLiquidGlassSupported ? undefined : CONTROL_EXITING} | ||
| 73 | > | ||
| 74 | {props.startedAt !== null && isLiquidGlassSupported ? ( | ||
| 75 | <UniwindLiquidGlassContainerView | ||
| 76 | spacing={GLASS_MERGE_SPACING} | ||
| 77 | pointerEvents="box-none" | ||
| 78 | className="flex-row items-center gap-4" | ||
| 79 | > | ||
| 80 | <AnimatedLiquidGlassView | ||
| 81 | colorScheme={props.colorScheme} | ||
| 82 | effect="regular" | ||
| 83 | pointerEvents="none" | ||
| 84 | className="h-11 justify-center overflow-hidden rounded-full" | ||
| 85 | style={timerStyle} | ||
| 86 | > | ||
| 87 | <WorkingDuration startedAt={props.startedAt} /> | ||
| 88 | </AnimatedLiquidGlassView> | ||
| 89 | |||
| 90 | <AnimatedLiquidGlassView | ||
| 91 | colorScheme={props.colorScheme} | ||
| 92 | effect="regular" | ||
| 93 | interactive | ||
| 94 | pointerEvents={props.showScrollToEnd ? "auto" : "none"} | ||
| 95 | accessibilityElementsHidden={!props.showScrollToEnd} | ||
| 96 | importantForAccessibility={props.showScrollToEnd ? "auto" : "no-hide-descendants"} | ||
| 97 | className="h-11 w-11 items-center justify-center overflow-hidden rounded-full" | ||
| 98 | style={arrowTransformStyle} | ||
| 99 | > | ||
| 100 | <Animated.View style={arrowContentStyle}> | ||
| 101 | <ScrollToEndButton disabled={!props.showScrollToEnd} onPress={props.onScrollToEnd} /> | ||
| 102 | </Animated.View> | ||
| 103 | </AnimatedLiquidGlassView> | ||
| 104 | </UniwindLiquidGlassContainerView> | ||
| 105 | ) : props.startedAt !== null ? ( | ||
| 106 | <View pointerEvents="box-none" className="flex-row items-center gap-4"> | ||
| 107 | <Animated.View | ||
| 108 | pointerEvents="none" | ||
| 109 | className="h-11 justify-center rounded-full border border-border bg-card shadow-md shadow-black/10" | ||
| 110 | style={timerStyle} | ||
| 111 | > | ||
| 112 | <WorkingDuration startedAt={props.startedAt} /> | ||
| 113 | </Animated.View> | ||
| 114 | |||
| 115 | <Animated.View | ||
| 116 | pointerEvents={props.showScrollToEnd ? "auto" : "none"} | ||
| 117 | accessibilityElementsHidden={!props.showScrollToEnd} | ||
| 118 | importantForAccessibility={props.showScrollToEnd ? "auto" : "no-hide-descendants"} | ||
| 119 | style={[arrowTransformStyle, arrowContentStyle]} | ||
| 120 | > | ||
| 121 | <ControlPill | ||
| 122 | accessibilityLabel="Scroll to end" | ||
| 123 | activateOnPressIn | ||
| 124 | className="h-11 w-11 border border-border bg-card shadow-md shadow-black/10" | ||
| 125 | disabled={!props.showScrollToEnd} | ||
| 126 | icon={{ ios: "chevron.down", android: "keyboard_arrow_down" }} | ||
| 127 | onPress={props.onScrollToEnd} | ||
| 128 | /> | ||
| 129 | </Animated.View> | ||
| 130 | </View> | ||
| 131 | ) : isLiquidGlassSupported ? ( | ||
| 132 | <UniwindLiquidGlassView | ||
| 133 | colorScheme={props.colorScheme} | ||
| 134 | effect="regular" | ||
| 135 | interactive | ||
| 136 | className="h-11 w-11 items-center justify-center overflow-hidden rounded-full" | ||
| 137 | > | ||
| 138 | <ScrollToEndButton onPress={props.onScrollToEnd} /> | ||
| 139 | </UniwindLiquidGlassView> | ||
| 140 | ) : ( | ||
| 141 | <ControlPill | ||
| 142 | accessibilityLabel="Scroll to end" | ||
| 143 | activateOnPressIn | ||
| 144 | className="h-11 w-11 border border-border bg-card shadow-md shadow-black/10" | ||
| 145 | icon={{ ios: "chevron.down", android: "keyboard_arrow_down" }} | ||
| 146 | onPress={props.onScrollToEnd} | ||
| 147 | /> | ||
| 148 | )} | ||
| 149 | </Animated.View> | ||
| 150 | ); | ||
| 151 | } | ||
| 152 | |||
| 153 | function WorkingDuration(props: { readonly startedAt: string }) { | ||
| 154 | const [nowMs, setNowMs] = useState(() => Date.now()); | ||
| 155 | |||
| 156 | useEffect(() => { | ||
| 157 | setNowMs(Date.now()); | ||
| 158 | const intervalId = setInterval(() => setNowMs(Date.now()), 1_000); | ||
| 159 | return () => clearInterval(intervalId); | ||
| 160 | }, [props.startedAt]); | ||
| 161 | |||
| 162 | const duration = formatWorkingDuration(props.startedAt, nowMs); | ||
| 163 | const label = `Working for ${duration}`; | ||
| 164 | |||
| 165 | return ( | ||
| 166 | <View accessible accessibilityLabel={label} className="h-11 flex-row items-center px-4"> | ||
| 167 | <Text className="font-t3-medium text-xs text-foreground">Working for </Text> | ||
| 168 | <SystemText | ||
| 169 | className="text-xs text-foreground" | ||
| 170 | style={{ fontVariant: ["tabular-nums"], fontWeight: "500" }} | ||
| 171 | > | ||
| 172 | {duration} | ||
| 173 | </SystemText> | ||
| 174 | </View> | ||
| 175 | ); | ||
| 176 | } | ||
| 177 | |||
| 178 | function formatWorkingDuration(startedAt: string, nowMs: number): string { | ||
| 179 | const startedAtMs = Date.parse(startedAt); | ||
| 180 | if (!Number.isFinite(startedAtMs) || nowMs <= startedAtMs) { | ||
| 181 | return "0s"; | ||
| 182 | } | ||
| 183 | |||
| 184 | const totalSeconds = Math.floor((nowMs - startedAtMs) / 1_000); | ||
| 185 | if (totalSeconds < 60) { | ||
| 186 | return `${totalSeconds}s`; | ||
| 187 | } | ||
| 188 | |||
| 189 | const minutes = Math.floor(totalSeconds / 60); | ||
| 190 | const seconds = String(totalSeconds % 60).padStart(2, "0"); | ||
| 191 | return `${minutes}m ${seconds}s`; | ||
| 192 | } | ||
| 193 | |||
| 194 | function ScrollToEndButton(props: { readonly disabled?: boolean; readonly onPress: () => void }) { | ||
| 195 | return ( | ||
| 196 | <ControlPill | ||
| 197 | accessibilityLabel="Scroll to end" | ||
| 198 | activateOnPressIn | ||
| 199 | className="h-11 w-11 bg-transparent" | ||
| 200 | disabled={props.disabled} | ||
| 201 | icon={{ ios: "chevron.down", android: "keyboard_arrow_down" }} | ||
| 202 | onPress={props.onPress} | ||
| 203 | /> | ||
| 204 | ); | ||
| 205 | } | ||
apps/mobile/src/features/threads/thread-work-log.tsx+311-125| ... | @@ -1,30 +1,201 @@ | ... | @@ -1,30 +1,201 @@ |
| 1 | import * as Haptics from "expo-haptics"; | 1 | import * as Haptics from "expo-haptics"; |
| 2 | import { type AppSymbolName, SymbolView } from "../../components/AppSymbol"; | 2 | import { type AppSymbolName, SymbolView } from "../../components/AppSymbol"; |
| 3 | import { LayoutAnimation, Pressable, ScrollView, View } from "react-native"; | 3 | import { MaskedView } from "@expo/ui/community/masked-view"; |
| 4 | import { useIsFocused } from "@react-navigation/native"; | ||
| 5 | import { useEffect, useId, useState, type ComponentProps } from "react"; | ||
| 6 | import { | ||
| 7 | AccessibilityInfo, | ||
| 8 | AppState, | ||
| 9 | type ColorValue, | ||
| 10 | Pressable, | ||
| 11 | ScrollView, | ||
| 12 | View, | ||
| 13 | } from "react-native"; | ||
| 14 | import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; | ||
| 4 | 15 | ||
| 5 | import { AppText as Text } from "../../components/AppText"; | 16 | import { AppText as Text } from "../../components/AppText"; |
| 6 | import { scaledTypographyLineHeight } from "../../lib/appearancePreferences"; | ||
| 7 | import { cn } from "../../lib/cn"; | 17 | import { cn } from "../../lib/cn"; |
| 8 | import type { ThreadFeedActivity } from "../../lib/threadActivity"; | 18 | import type { ThreadFeedActivity } from "../../lib/threadActivity"; |
| 9 | import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; | 19 | import type { ToolGroupSummaryKind } from "@t3tools/client-runtime/work-log/presentation"; |
| 10 | import Animated, { FadeIn } from "react-native-reanimated"; | 20 | import Animated, { |
| 21 | cancelAnimation, | ||
| 22 | Easing, | ||
| 23 | FadeIn, | ||
| 24 | FadeOut, | ||
| 25 | LinearTransition, | ||
| 26 | ReduceMotion, | ||
| 27 | useAnimatedStyle, | ||
| 28 | useSharedValue, | ||
| 29 | withDelay, | ||
| 30 | withRepeat, | ||
| 31 | withSequence, | ||
| 32 | withTiming, | ||
| 33 | } from "react-native-reanimated"; | ||
| 11 | 34 | ||
| 12 | const WORK_LOG_LAYOUT_ANIMATION = { | 35 | const SHIMMER_WIDTH = 72; |
| 13 | duration: 180, | 36 | const SHIMMER_SWEEP_MS = 1_350; |
| 14 | create: { | 37 | const SHIMMER_PAUSE_MS = 1_450; |
| 15 | type: LayoutAnimation.Types.easeInEaseOut, | 38 | const SHIMMER_ICON_AND_GAP_WIDTH = 30; |
| 16 | property: LayoutAnimation.Properties.opacity, | 39 | export const THREAD_DISCLOSURE_TRANSITION_MS = 180; |
| 17 | }, | 40 | const WORK_LOG_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_TRANSITION_MS); |
| 18 | update: { type: LayoutAnimation.Types.easeInEaseOut }, | 41 | const WORK_LOG_DETAIL_ENTER_TRANSITION = FadeIn.duration(140); |
| 19 | delete: { | 42 | const WORK_LOG_DETAIL_EXIT_TRANSITION = FadeOut.duration(120); |
| 20 | type: LayoutAnimation.Types.easeInEaseOut, | ||
| 21 | property: LayoutAnimation.Properties.opacity, | ||
| 22 | }, | ||
| 23 | } as const; | ||
| 24 | 43 | ||
| 25 | function triggerDisclosureFeedback() { | 44 | function ShimmerWorkContent(props: { |
| 26 | LayoutAnimation.configureNext(WORK_LOG_LAYOUT_ANIMATION); | 45 | readonly highlighted: boolean; |
| 27 | void Haptics.selectionAsync(); | 46 | readonly icon: AppSymbolName; |
| 47 | readonly iconSubtleColor: ColorValue; | ||
| 48 | readonly label: string; | ||
| 49 | readonly onTextLayout?: ComponentProps<typeof Text>["onTextLayout"]; | ||
| 50 | readonly showIcon: boolean; | ||
| 51 | }) { | ||
| 52 | return ( | ||
| 53 | <View className="flex-row items-center gap-1.5"> | ||
| 54 | <View className="h-6 w-6 shrink-0 items-center justify-center"> | ||
| 55 | {props.showIcon ? ( | ||
| 56 | <SymbolView | ||
| 57 | name={props.icon} | ||
| 58 | size={14} | ||
| 59 | weight="medium" | ||
| 60 | {...(props.highlighted | ||
| 61 | ? { tintColorClassName: "accent-foreground" as const } | ||
| 62 | : { tintColor: props.iconSubtleColor })} | ||
| 63 | type="monochrome" | ||
| 64 | /> | ||
| 65 | ) : null} | ||
| 66 | </View> | ||
| 67 | <Text | ||
| 68 | className={cn( | ||
| 69 | "min-w-0 shrink text-sm", | ||
| 70 | props.highlighted ? "text-foreground" : "text-foreground-muted", | ||
| 71 | )} | ||
| 72 | numberOfLines={1} | ||
| 73 | onTextLayout={props.onTextLayout} | ||
| 74 | > | ||
| 75 | {props.label} | ||
| 76 | </Text> | ||
| 77 | </View> | ||
| 78 | ); | ||
| 79 | } | ||
| 80 | |||
| 81 | function ShimmeringWorkContent(props: { | ||
| 82 | readonly icon: AppSymbolName; | ||
| 83 | readonly iconSubtleColor: ColorValue; | ||
| 84 | readonly label: string; | ||
| 85 | readonly showIcon: boolean; | ||
| 86 | }) { | ||
| 87 | const [availableWidth, setAvailableWidth] = useState(0); | ||
| 88 | const [textWidth, setTextWidth] = useState(0); | ||
| 89 | const [appIsActive, setAppIsActive] = useState(AppState.currentState === "active"); | ||
| 90 | const [reducedMotion, setReducedMotion] = useState(true); | ||
| 91 | const screenIsFocused = useIsFocused(); | ||
| 92 | const progress = useSharedValue(0); | ||
| 93 | const gradientId = `work-shimmer-${useId().replaceAll(":", "")}`; | ||
| 94 | const contentWidth = Math.min(availableWidth, SHIMMER_ICON_AND_GAP_WIDTH + Math.ceil(textWidth)); | ||
| 95 | |||
| 96 | useEffect(() => { | ||
| 97 | const subscription = AppState.addEventListener("change", (state) => { | ||
| 98 | setAppIsActive(state === "active"); | ||
| 99 | }); | ||
| 100 | return () => subscription.remove(); | ||
| 101 | }, []); | ||
| 102 | |||
| 103 | useEffect(() => { | ||
| 104 | void AccessibilityInfo.isReduceMotionEnabled().then(setReducedMotion); | ||
| 105 | const subscription = AccessibilityInfo.addEventListener( | ||
| 106 | "reduceMotionChanged", | ||
| 107 | setReducedMotion, | ||
| 108 | ); | ||
| 109 | return () => subscription.remove(); | ||
| 110 | }, []); | ||
| 111 | |||
| 112 | useEffect(() => { | ||
| 113 | cancelAnimation(progress); | ||
| 114 | progress.value = 0; | ||
| 115 | if (contentWidth <= 0 || reducedMotion || !appIsActive || !screenIsFocused) return; | ||
| 116 | |||
| 117 | progress.value = withRepeat( | ||
| 118 | withSequence( | ||
| 119 | withTiming(1, { | ||
| 120 | duration: SHIMMER_SWEEP_MS, | ||
| 121 | easing: Easing.linear, | ||
| 122 | reduceMotion: ReduceMotion.Never, | ||
| 123 | }), | ||
| 124 | withDelay( | ||
| 125 | SHIMMER_PAUSE_MS, | ||
| 126 | withTiming(0, { duration: 0, reduceMotion: ReduceMotion.Never }), | ||
| 127 | ), | ||
| 128 | ), | ||
| 129 | -1, | ||
| 130 | false, | ||
| 131 | undefined, | ||
| 132 | ReduceMotion.Never, | ||
| 133 | ); | ||
| 134 | return () => cancelAnimation(progress); | ||
| 135 | }, [appIsActive, contentWidth, progress, reducedMotion, screenIsFocused]); | ||
| 136 | |||
| 137 | const sweepStyle = useAnimatedStyle(() => ({ | ||
| 138 | transform: [{ translateX: -SHIMMER_WIDTH + progress.value * (contentWidth + SHIMMER_WIDTH) }], | ||
| 139 | })); | ||
| 140 | const counterSweepStyle = useAnimatedStyle(() => ({ | ||
| 141 | transform: [{ translateX: SHIMMER_WIDTH - progress.value * (contentWidth + SHIMMER_WIDTH) }], | ||
| 142 | })); | ||
| 143 | |||
| 144 | return ( | ||
| 145 | <View | ||
| 146 | className="min-w-0 flex-1" | ||
| 147 | onLayout={(event) => setAvailableWidth(event.nativeEvent.layout.width)} | ||
| 148 | > | ||
| 149 | <ShimmerWorkContent | ||
| 150 | highlighted={false} | ||
| 151 | icon={props.icon} | ||
| 152 | iconSubtleColor={props.iconSubtleColor} | ||
| 153 | label={props.label} | ||
| 154 | showIcon={props.showIcon} | ||
| 155 | onTextLayout={(event) => setTextWidth(event.nativeEvent.lines[0]?.width ?? 0)} | ||
| 156 | /> | ||
| 157 | {!reducedMotion && appIsActive && screenIsFocused && contentWidth > 0 ? ( | ||
| 158 | <Animated.View | ||
| 159 | className="absolute inset-y-0 left-0 overflow-hidden" | ||
| 160 | pointerEvents="none" | ||
| 161 | accessible={false} | ||
| 162 | accessibilityElementsHidden | ||
| 163 | importantForAccessibility="no-hide-descendants" | ||
| 164 | style={[{ width: SHIMMER_WIDTH }, sweepStyle]} | ||
| 165 | > | ||
| 166 | <MaskedView | ||
| 167 | className="absolute inset-0" | ||
| 168 | maskElement={ | ||
| 169 | <Svg width="100%" height="100%"> | ||
| 170 | <Defs> | ||
| 171 | <LinearGradient id={gradientId} x1="0%" x2="100%" y1="0%" y2="0%"> | ||
| 172 | <Stop offset="0" stopColor="white" stopOpacity={0} /> | ||
| 173 | <Stop offset="0.15" stopColor="white" stopOpacity={0.12} /> | ||
| 174 | <Stop offset="0.35" stopColor="white" stopOpacity={0.55} /> | ||
| 175 | <Stop offset="0.5" stopColor="white" stopOpacity={1} /> | ||
| 176 | <Stop offset="0.65" stopColor="white" stopOpacity={0.55} /> | ||
| 177 | <Stop offset="0.85" stopColor="white" stopOpacity={0.12} /> | ||
| 178 | <Stop offset="1" stopColor="white" stopOpacity={0} /> | ||
| 179 | </LinearGradient> | ||
| 180 | </Defs> | ||
| 181 | <Rect width="100%" height="100%" fill={`url(#${gradientId})`} /> | ||
| 182 | </Svg> | ||
| 183 | } | ||
| 184 | > | ||
| 185 | <Animated.View style={[{ width: availableWidth }, counterSweepStyle]}> | ||
| 186 | <ShimmerWorkContent | ||
| 187 | highlighted | ||
| 188 | icon={props.icon} | ||
| 189 | iconSubtleColor={props.iconSubtleColor} | ||
| 190 | label={props.label} | ||
| 191 | showIcon={props.showIcon} | ||
| 192 | /> | ||
| 193 | </Animated.View> | ||
| 194 | </MaskedView> | ||
| 195 | </Animated.View> | ||
| 196 | ) : null} | ||
| 197 | </View> | ||
| 198 | ); | ||
| 28 | } | 199 | } |
| 29 | 200 | ||
| 30 | function stripShellWrapper(value: string): string { | 201 | function stripShellWrapper(value: string): string { |
| ... | @@ -79,44 +250,23 @@ function isFreshRow(createdAt: string): boolean { | ... | @@ -79,44 +250,23 @@ function isFreshRow(createdAt: string): boolean { |
| 79 | return Number.isFinite(timestamp) && Date.now() - timestamp < FRESH_ROW_WINDOW_MS; | 250 | return Number.isFinite(timestamp) && Date.now() - timestamp < FRESH_ROW_WINDOW_MS; |
| 80 | } | 251 | } |
| 81 | 252 | ||
| 82 | // Tool-like activities with a neutral status carry no signal worth a row. | ||
| 83 | export function visibleWorkLogActivities( | ||
| 84 | activities: ReadonlyArray<ThreadFeedActivity>, | ||
| 85 | ): ReadonlyArray<ThreadFeedActivity> { | ||
| 86 | return activities.filter((activity) => !(activity.toolLike && activity.status === "neutral")); | ||
| 87 | } | ||
| 88 | |||
| 89 | // Pre-measurement heights for the feed's getFixedItemSize. Collapsed work-log | 253 | // Pre-measurement heights for the feed's getFixedItemSize. Collapsed work-log |
| 90 | // rows are single-line (numberOfLines={1}) inside a min-height that stays | 254 | // rows are single-line (numberOfLines={1}) inside a min-height that stays |
| 91 | // taller than the text at every supported base font size (text-xs reaches | 255 | // taller than text-sm at every supported base font size, so row height is |
| 92 | // 23px at the 22pt maximum, under the 32px min-h-8), so row height is | 256 | // deterministic. Values mirror the classNames below. A mismatch only costs a |
| 93 | // deterministic. The "work log" label has no such clamp — its height follows | 257 | // one-time correction on measure. |
| 94 | // the scaled text-2xs line height. Values mirror the classNames below — keep | ||
| 95 | // them in sync; a mismatch only costs a one-time correction on measure. | ||
| 96 | const WORK_ROW_HEIGHT = 32; // min-h-8 | 258 | const WORK_ROW_HEIGHT = 32; // min-h-8 |
| 97 | const WORK_ROW_GAP = 1; // gap-px | 259 | const WORK_ROW_GAP = 1; // gap-px |
| 98 | const WORK_LOG_HEADER_PADDING = 2; // pb-0.5 under the "work log" label | ||
| 99 | const WORK_LOG_BOTTOM_MARGIN = 4; // mb-1 | 260 | const WORK_LOG_BOTTOM_MARGIN = 4; // mb-1 |
| 100 | 261 | ||
| 101 | export const WORK_GROUP_TOGGLE_HEIGHT = 36; // min-h-8 (32) + mb-1 (4) | 262 | export const WORK_GROUP_TOGGLE_HEIGHT = 36; // min-h-8 (32) + mb-1 (4) |
| 102 | 263 | ||
| 103 | export function collapsedWorkLogHeight( | 264 | export function collapsedWorkLogHeight(activities: ReadonlyArray<ThreadFeedActivity>): number { |
| 104 | activities: ReadonlyArray<ThreadFeedActivity>, | 265 | const rows = activities; |
| 105 | baseFontSize: number, | ||
| 106 | ): number { | ||
| 107 | const rows = visibleWorkLogActivities(activities); | ||
| 108 | if (rows.length === 0) { | 266 | if (rows.length === 0) { |
| 109 | return 0; | 267 | return 0; |
| 110 | } | 268 | } |
| 111 | const onlyToolRows = rows.every((row) => row.toolLike); | 269 | return WORK_LOG_BOTTOM_MARGIN + rows.length * WORK_ROW_HEIGHT + (rows.length - 1) * WORK_ROW_GAP; |
| 112 | const headerHeight = | ||
| 113 | scaledTypographyLineHeight(MOBILE_TYPOGRAPHY.caption, baseFontSize) + WORK_LOG_HEADER_PADDING; | ||
| 114 | return ( | ||
| 115 | WORK_LOG_BOTTOM_MARGIN + | ||
| 116 | (onlyToolRows ? 0 : headerHeight) + | ||
| 117 | rows.length * WORK_ROW_HEIGHT + | ||
| 118 | (rows.length - 1) * WORK_ROW_GAP | ||
| 119 | ); | ||
| 120 | } | 270 | } |
| 121 | 271 | ||
| 122 | export function ThreadWorkLog(props: { | 272 | export function ThreadWorkLog(props: { |
| ... | @@ -127,7 +277,7 @@ export function ThreadWorkLog(props: { | ... | @@ -127,7 +277,7 @@ export function ThreadWorkLog(props: { |
| 127 | readonly onCopyRow: (rowId: string, value: string) => void; | 277 | readonly onCopyRow: (rowId: string, value: string) => void; |
| 128 | readonly onToggleRow: (rowId: string) => void; | 278 | readonly onToggleRow: (rowId: string) => void; |
| 129 | }) { | 279 | }) { |
| 130 | const rows = visibleWorkLogActivities(props.activities).map((activity) => ({ | 280 | const rows = props.activities.map((activity) => ({ |
| 131 | ...activity, | 281 | ...activity, |
| 132 | detail: compactActivityDetail(activity.detail), | 282 | detail: compactActivityDetail(activity.detail), |
| 133 | })); | 283 | })); |
| ... | @@ -136,32 +286,28 @@ export function ThreadWorkLog(props: { | ... | @@ -136,32 +286,28 @@ export function ThreadWorkLog(props: { |
| 136 | return null; | 286 | return null; |
| 137 | } | 287 | } |
| 138 | 288 | ||
| 139 | const onlyToolRows = rows.every((row) => row.toolLike); | ||
| 140 | |||
| 141 | return ( | 289 | return ( |
| 142 | <View className="-mx-1 mb-1 px-1 py-0"> | 290 | <View className="-mx-1 mb-1 px-1 py-0"> |
| 143 | {!onlyToolRows ? ( | ||
| 144 | <Text className="px-0.5 pb-0.5 font-t3-medium text-2xs text-foreground-muted opacity-60"> | ||
| 145 | work log | ||
| 146 | </Text> | ||
| 147 | ) : null} | ||
| 148 | |||
| 149 | <View className="gap-px"> | 291 | <View className="gap-px"> |
| 150 | {rows.map((row) => { | 292 | {rows.map((row) => { |
| 151 | const expanded = props.expandedRows[row.id] ?? false; | 293 | const expanded = props.expandedRows[row.id] ?? false; |
| 152 | const canExpand = row.canExpand; | 294 | const canExpand = row.canExpand; |
| 153 | const fullDetail = expanded ? row.getFullDetail() : null; | 295 | const fullDetail = expanded ? row.getFullDetail() : null; |
| 154 | const displayText = row.detail ? `${row.summary} ${row.detail}` : row.summary; | 296 | const displayText = row.detail ?? row.summary; |
| 155 | const iconIsDestructive = row.icon === "alert" || row.icon === "warning"; | 297 | const iconIsDestructive = row.icon === "alert" || row.icon === "warning"; |
| 298 | const failed = row.status === "failure"; | ||
| 299 | const showIcon = !row.groupedToolDetail || iconIsDestructive || failed; | ||
| 156 | 300 | ||
| 157 | return ( | 301 | return ( |
| 158 | <Animated.View | 302 | <Animated.View |
| 159 | key={row.id} | 303 | key={row.id} |
| 304 | layout={WORK_LOG_LAYOUT_TRANSITION} | ||
| 305 | className="overflow-hidden" | ||
| 160 | {...(isFreshRow(row.createdAt) ? { entering: FadeIn.duration(200) } : {})} | 306 | {...(isFreshRow(row.createdAt) ? { entering: FadeIn.duration(200) } : {})} |
| 161 | > | 307 | > |
| 162 | <Pressable | 308 | <Pressable |
| 163 | accessibilityRole={canExpand ? "button" : undefined} | 309 | accessibilityRole={canExpand ? "button" : undefined} |
| 164 | accessibilityLabel={displayText} | 310 | accessibilityLabel={failed ? `${displayText}, tool call failed` : displayText} |
| 165 | accessibilityHint={ | 311 | accessibilityHint={ |
| 166 | canExpand | 312 | canExpand |
| 167 | ? "Double tap to show full details. Long press to copy." | 313 | ? "Double tap to show full details. Long press to copy." |
| ... | @@ -171,7 +317,7 @@ export function ThreadWorkLog(props: { | ... | @@ -171,7 +317,7 @@ export function ThreadWorkLog(props: { |
| 171 | hitSlop={4} | 317 | hitSlop={4} |
| 172 | onPress={() => { | 318 | onPress={() => { |
| 173 | if (canExpand) { | 319 | if (canExpand) { |
| 174 | triggerDisclosureFeedback(); | 320 | void Haptics.selectionAsync(); |
| 175 | props.onToggleRow(row.id); | 321 | props.onToggleRow(row.id); |
| 176 | } | 322 | } |
| 177 | }} | 323 | }} |
| ... | @@ -179,29 +325,41 @@ export function ThreadWorkLog(props: { | ... | @@ -179,29 +325,41 @@ export function ThreadWorkLog(props: { |
| 179 | className="rounded-md px-0.5 py-0 active:bg-subtle" | 325 | className="rounded-md px-0.5 py-0 active:bg-subtle" |
| 180 | > | 326 | > |
| 181 | <View className="min-h-8 flex-row items-center gap-1.5"> | 327 | <View className="min-h-8 flex-row items-center gap-1.5"> |
| 182 | <View className="h-[18px] w-5 shrink-0 items-center justify-center"> | 328 | {row.live ? ( |
| 183 | <SymbolView | 329 | <ShimmeringWorkContent |
| 184 | name={workRowSymbolName(row.icon)} | 330 | icon={workRowSymbolName(row.icon)} |
| 185 | size={13} | 331 | iconSubtleColor={props.iconSubtleColor} |
| 186 | weight="medium" | 332 | label={displayText} |
| 187 | tintColor={iconIsDestructive ? "#e11d48" : props.iconSubtleColor} | 333 | showIcon={showIcon} |
| 188 | type="monochrome" | ||
| 189 | /> | 334 | /> |
| 190 | </View> | 335 | ) : ( |
| 191 | 336 | <> | |
| 192 | <Text className="min-w-0 flex-1 text-xs text-foreground" numberOfLines={1}> | 337 | <View className="h-6 w-6 shrink-0 items-center justify-center"> |
| 193 | <Text | 338 | {showIcon ? ( |
| 194 | className={cn( | 339 | <SymbolView |
| 195 | "font-t3-medium text-foreground", | 340 | name={ |
| 196 | iconIsDestructive && "text-adaptive-rose-600-400", | 341 | failed |
| 197 | )} | 342 | ? { ios: "xmark", android: "close" } |
| 198 | > | 343 | : workRowSymbolName(row.icon) |
| 199 | {row.summary} | 344 | } |
| 200 | </Text> | 345 | size={14} |
| 201 | {row.detail ? ( | 346 | weight="medium" |
| 202 | <Text className="text-foreground-muted opacity-60"> {row.detail}</Text> | 347 | tintColor={iconIsDestructive ? "#e11d48" : props.iconSubtleColor} |
| 203 | ) : null} | 348 | type="monochrome" |
| 204 | </Text> | 349 | /> |
| 350 | ) : null} | ||
| 351 | </View> | ||
| 352 | <Text | ||
| 353 | className={cn( | ||
| 354 | "min-w-0 flex-1 text-sm text-foreground-muted", | ||
| 355 | iconIsDestructive && "font-t3-medium text-adaptive-rose-600-400", | ||
| 356 | )} | ||
| 357 | numberOfLines={1} | ||
| 358 | > | ||
| 359 | {displayText} | ||
| 360 | </Text> | ||
| 361 | </> | ||
| 362 | )} | ||
| 205 | 363 | ||
| 206 | <View className="shrink-0 flex-row items-center gap-px"> | 364 | <View className="shrink-0 flex-row items-center gap-px"> |
| 207 | {props.copiedRowId === row.id ? ( | 365 | {props.copiedRowId === row.id ? ( |
| ... | @@ -223,28 +381,17 @@ export function ThreadWorkLog(props: { | ... | @@ -223,28 +381,17 @@ export function ThreadWorkLog(props: { |
| 223 | /> | 381 | /> |
| 224 | ) : null} | 382 | ) : null} |
| 225 | </View> | 383 | </View> |
| 226 | <View className="h-4 w-4 items-center justify-center"> | ||
| 227 | {row.status ? ( | ||
| 228 | <SymbolView | ||
| 229 | name={ | ||
| 230 | row.status === "failure" | ||
| 231 | ? { ios: "xmark", android: "close" } | ||
| 232 | : row.status === "success" | ||
| 233 | ? { ios: "checkmark", android: "check" } | ||
| 234 | : { ios: "minus", android: "remove" } | ||
| 235 | } | ||
| 236 | size={11} | ||
| 237 | tintColor={props.iconSubtleColor} | ||
| 238 | type="monochrome" | ||
| 239 | /> | ||
| 240 | ) : null} | ||
| 241 | </View> | ||
| 242 | </View> | 384 | </View> |
| 243 | </View> | 385 | </View> |
| 244 | </Pressable> | 386 | </Pressable> |
| 245 | 387 | ||
| 246 | {fullDetail ? ( | 388 | {fullDetail ? ( |
| 247 | <View className="ml-7 border-l border-adaptive-neutral-300-a60-white-a12 pb-1 pl-3 pt-0.5"> | 389 | <Animated.View |
| 390 | entering={WORK_LOG_DETAIL_ENTER_TRANSITION} | ||
| 391 | exiting={WORK_LOG_DETAIL_EXIT_TRANSITION} | ||
| 392 | layout={WORK_LOG_LAYOUT_TRANSITION} | ||
| 393 | className="ml-7 border-l border-adaptive-neutral-300-a60-white-a12 pb-1 pl-3 pt-0.5" | ||
| 394 | > | ||
| 248 | <ScrollView | 395 | <ScrollView |
| 249 | nestedScrollEnabled | 396 | nestedScrollEnabled |
| 250 | directionalLockEnabled | 397 | directionalLockEnabled |
| ... | @@ -259,7 +406,7 @@ export function ThreadWorkLog(props: { | ... | @@ -259,7 +406,7 @@ export function ThreadWorkLog(props: { |
| 259 | {fullDetail} | 406 | {fullDetail} |
| 260 | </Text> | 407 | </Text> |
| 261 | </ScrollView> | 408 | </ScrollView> |
| 262 | </View> | 409 | </Animated.View> |
| 263 | ) : null} | 410 | ) : null} |
| 264 | </Animated.View> | 411 | </Animated.View> |
| 265 | ); | 412 | ); |
| ... | @@ -273,27 +420,24 @@ export function ThreadWorkGroupToggle(props: { | ... | @@ -273,27 +420,24 @@ export function ThreadWorkGroupToggle(props: { |
| 273 | readonly expanded: boolean; | 420 | readonly expanded: boolean; |
| 274 | readonly hiddenCount: number; | 421 | readonly hiddenCount: number; |
| 275 | readonly iconSubtleColor: import("react-native").ColorValue; | 422 | readonly iconSubtleColor: import("react-native").ColorValue; |
| 276 | readonly onlyToolActivities: boolean; | 423 | readonly summary: string; |
| 424 | readonly summaryKind: ToolGroupSummaryKind; | ||
| 425 | readonly hasFailure: boolean; | ||
| 426 | readonly shimmer: boolean; | ||
| 277 | readonly onToggle: () => void; | 427 | readonly onToggle: () => void; |
| 278 | }) { | 428 | }) { |
| 279 | const noun = props.onlyToolActivities | 429 | const accessibilityLabel = props.hasFailure |
| 280 | ? props.hiddenCount === 1 | 430 | ? `${props.summary}, tool call failed` |
| 281 | ? "tool call" | 431 | : props.summary; |
| 282 | : "tool calls" | 432 | const icon = toolGroupSummarySymbolName(props.summaryKind); |
| 283 | : props.hiddenCount === 1 | ||
| 284 | ? "log entry" | ||
| 285 | : "log entries"; | ||
| 286 | const collapsedLabel = `Show ${props.hiddenCount} previous ${noun}`; | ||
| 287 | const expandedLabel = props.onlyToolActivities | ||
| 288 | ? "Show fewer tool calls" | ||
| 289 | : "Show fewer log entries"; | ||
| 290 | 433 | ||
| 291 | return ( | 434 | return ( |
| 292 | <View className="-mx-1 mb-1 px-1 py-0"> | 435 | <View className="-mx-1 mb-1 px-1 py-0"> |
| 293 | <Pressable | 436 | <Pressable |
| 294 | accessibilityRole="button" | 437 | accessibilityRole="button" |
| 295 | accessibilityState={{ expanded: props.expanded }} | 438 | accessibilityState={{ expanded: props.expanded }} |
| 296 | accessibilityLabel={props.expanded ? expandedLabel : collapsedLabel} | 439 | accessibilityLabel={accessibilityLabel} |
| 440 | accessibilityHint={`Double tap to ${props.expanded ? "hide" : "show"} ${props.hiddenCount} tool ${props.hiddenCount === 1 ? "call" : "calls"}.`} | ||
| 297 | hitSlop={4} | 441 | hitSlop={4} |
| 298 | onPress={() => { | 442 | onPress={() => { |
| 299 | void Haptics.selectionAsync(); | 443 | void Haptics.selectionAsync(); |
| ... | @@ -301,22 +445,64 @@ export function ThreadWorkGroupToggle(props: { | ... | @@ -301,22 +445,64 @@ export function ThreadWorkGroupToggle(props: { |
| 301 | }} | 445 | }} |
| 302 | className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0 active:bg-subtle" | 446 | className="min-h-8 flex-row items-center gap-1.5 rounded-md px-0.5 py-0 active:bg-subtle" |
| 303 | > | 447 | > |
| 304 | <View className="h-[18px] w-5 items-center justify-center"> | 448 | {props.shimmer ? ( |
| 305 | <SymbolView | 449 | <ShimmeringWorkContent |
| 306 | name={ | 450 | icon={icon} |
| 307 | props.expanded | 451 | iconSubtleColor={props.iconSubtleColor} |
| 308 | ? { ios: "chevron.up", android: "keyboard_arrow_up" } | 452 | label={props.summary} |
| 309 | : { ios: "chevron.down", android: "keyboard_arrow_down" } | 453 | showIcon |
| 310 | } | ||
| 311 | size={12} | ||
| 312 | tintColor={props.iconSubtleColor} | ||
| 313 | type="monochrome" | ||
| 314 | /> | 454 | /> |
| 315 | </View> | 455 | ) : ( |
| 316 | <Text className="font-t3-medium text-xs text-foreground opacity-80"> | 456 | <> |
| 317 | {props.expanded ? expandedLabel : `+${props.hiddenCount} previous ${noun}`} | 457 | <View className="h-6 w-6 items-center justify-center"> |
| 318 | </Text> | 458 | <SymbolView |
| 459 | name={icon} | ||
| 460 | size={14} | ||
| 461 | tintColor={props.iconSubtleColor} | ||
| 462 | type="monochrome" | ||
| 463 | /> | ||
| 464 | </View> | ||
| 465 | <Text className="min-w-0 flex-1 text-sm text-foreground-muted" numberOfLines={1}> | ||
| 466 | {props.summary} | ||
| 467 | </Text> | ||
| 468 | </> | ||
| 469 | )} | ||
| 470 | <SymbolView | ||
| 471 | name={ | ||
| 472 | props.expanded | ||
| 473 | ? { ios: "chevron.up", android: "keyboard_arrow_up" } | ||
| 474 | : { ios: "chevron.down", android: "keyboard_arrow_down" } | ||
| 475 | } | ||
| 476 | size={11} | ||
| 477 | tintColor={props.iconSubtleColor} | ||
| 478 | type="monochrome" | ||
| 479 | /> | ||
| 319 | </Pressable> | 480 | </Pressable> |
| 320 | </View> | 481 | </View> |
| 321 | ); | 482 | ); |
| 322 | } | 483 | } |
| 484 | |||
| 485 | function toolGroupSummarySymbolName(kind: ToolGroupSummaryKind): AppSymbolName { | ||
| 486 | switch (kind) { | ||
| 487 | case "read": | ||
| 488 | return { ios: "eye", android: "visibility" }; | ||
| 489 | case "edit": | ||
| 490 | return { ios: "square.and.pencil", android: "edit" }; | ||
| 491 | case "command": | ||
| 492 | return { ios: "terminal", android: "terminal" }; | ||
| 493 | case "search": | ||
| 494 | return { ios: "globe", android: "public" }; | ||
| 495 | case "code-search": | ||
| 496 | return "magnifyingglass"; | ||
| 497 | case "other": | ||
| 498 | return { ios: "wrench", android: "build" }; | ||
| 499 | case "agent-tool": | ||
| 500 | return { ios: "sparkles", android: "auto_awesome" }; | ||
| 501 | case "tone-tool": | ||
| 502 | return { ios: "bolt", android: "bolt" }; | ||
| 503 | case "dynamic-tool": | ||
| 504 | case "update": | ||
| 505 | case "mixed": | ||
| 506 | return { ios: "hammer", android: "construction" }; | ||
| 507 | } | ||
| 508 | } |
apps/mobile/src/lib/threadActivity.test.ts+242-26| ... | @@ -411,8 +411,8 @@ describe("buildThreadFeed", () => { | ... | @@ -411,8 +411,8 @@ describe("buildThreadFeed", () => { |
| 411 | 411 | ||
| 412 | expect(group.activities).toHaveLength(1); | 412 | expect(group.activities).toHaveLength(1); |
| 413 | expect(group.activities[0]).toMatchObject({ | 413 | expect(group.activities[0]).toMatchObject({ |
| 414 | id: "tool-completed", | 414 | id: "tool-updated", |
| 415 | createdAt: "2026-04-01T00:00:02.000Z", | 415 | createdAt: "2026-04-01T00:00:01.000Z", |
| 416 | turnId: "turn-1", | 416 | turnId: "turn-1", |
| 417 | summary: "Run tests", | 417 | summary: "Run tests", |
| 418 | detail: "bun run test", | 418 | detail: "bun run test", |
| ... | @@ -592,7 +592,7 @@ describe("buildThreadFeed", () => { | ... | @@ -592,7 +592,7 @@ describe("buildThreadFeed", () => { |
| 592 | expect(expanded.map((entry) => entry.id)).toEqual([ | 592 | expect(expanded.map((entry) => entry.id)).toEqual([ |
| 593 | "assistant-first", | 593 | "assistant-first", |
| 594 | "turn-fold:turn-1", | 594 | "turn-fold:turn-1", |
| 595 | "tool-completed", | 595 | "work-toggle:work-group:tool-completed", |
| 596 | "assistant-final", | 596 | "assistant-final", |
| 597 | ]); | 597 | ]); |
| 598 | }); | 598 | }); |
| ... | @@ -745,6 +745,20 @@ describe("buildThreadFeed", () => { | ... | @@ -745,6 +745,20 @@ describe("buildThreadFeed", () => { |
| 745 | assistantMessageId: null, | 745 | assistantMessageId: null, |
| 746 | }, | 746 | }, |
| 747 | activities: [ | 747 | activities: [ |
| 748 | makeActivity({ | ||
| 749 | id: EventId.make("tool-succeeded"), | ||
| 750 | kind: "tool.completed", | ||
| 751 | tone: "tool", | ||
| 752 | summary: "Run command", | ||
| 753 | createdAt: "2026-04-01T00:00:04.000Z", | ||
| 754 | turnId, | ||
| 755 | payload: { | ||
| 756 | title: "Run command", | ||
| 757 | itemType: "command_execution", | ||
| 758 | detail: "done", | ||
| 759 | status: "completed", | ||
| 760 | }, | ||
| 761 | }), | ||
| 748 | makeActivity({ | 762 | makeActivity({ |
| 749 | id: EventId.make("tool-failed"), | 763 | id: EventId.make("tool-failed"), |
| 750 | kind: "tool.completed", | 764 | kind: "tool.completed", |
| ... | @@ -763,25 +777,26 @@ describe("buildThreadFeed", () => { | ... | @@ -763,25 +777,26 @@ describe("buildThreadFeed", () => { |
| 763 | }); | 777 | }); |
| 764 | 778 | ||
| 765 | const feed = buildThreadFeed(thread); | 779 | const feed = buildThreadFeed(thread); |
| 766 | expect(deriveThreadFeedPresentation(feed, thread.latestTurn, new Set())).toEqual(feed); | 780 | expect(deriveThreadFeedPresentation(feed, thread.latestTurn, new Set())).toMatchObject([ |
| 767 | expect(feed[0]).toMatchObject({ | ||
| 768 | type: "activity-group", | ||
| 769 | activities: [{ status: "failure" }], | ||
| 770 | }); | ||
| 771 | }); | ||
| 772 | |||
| 773 | it("appends active work as a normal timeline row", () => { | ||
| 774 | const startedAt = "2026-04-01T00:00:01.000Z"; | ||
| 775 | const presented = deriveThreadFeedPresentation([], null, new Set(), new Set(), startedAt); | ||
| 776 | |||
| 777 | expect(presented).toEqual([ | ||
| 778 | { | 781 | { |
| 779 | type: "working", | 782 | type: "work-toggle", |
| 780 | id: "working-indicator-row", | 783 | summary: "Ran 2 commands", |
| 781 | createdAt: startedAt, | 784 | hiddenCount: 2, |
| 785 | hasFailure: true, | ||
| 782 | }, | 786 | }, |
| 783 | ]); | 787 | ]); |
| 784 | expect(deriveThreadFeedPresentation(presented, null, new Set())).toEqual([]); | 788 | expect(feed[0]).toMatchObject({ |
| 789 | type: "activity-group", | ||
| 790 | activities: [{ status: "success" }, { status: "failure" }], | ||
| 791 | }); | ||
| 792 | expect( | ||
| 793 | deriveThreadFeedPresentation( | ||
| 794 | feed, | ||
| 795 | thread.latestTurn, | ||
| 796 | new Set(), | ||
| 797 | new Set(["work-group:tool-succeeded"]), | ||
| 798 | ).map((entry) => entry.id), | ||
| 799 | ).toEqual(["work-toggle:work-group:tool-succeeded", "tool-succeeded", "tool-failed"]); | ||
| 785 | }); | 800 | }); |
| 786 | 801 | ||
| 787 | it("models work-log overflow as list rows", () => { | 802 | it("models work-log overflow as list rows", () => { |
| ... | @@ -801,6 +816,14 @@ describe("buildThreadFeed", () => { | ... | @@ -801,6 +816,14 @@ describe("buildThreadFeed", () => { |
| 801 | icon: "command", | 816 | icon: "command", |
| 802 | toolLike: true, | 817 | toolLike: true, |
| 803 | status, | 818 | status, |
| 819 | workEntry: { | ||
| 820 | id, | ||
| 821 | createdAt, | ||
| 822 | turnId: null, | ||
| 823 | label: `Tool ${id}`, | ||
| 824 | command: `command ${id}`, | ||
| 825 | tone: "tool", | ||
| 826 | }, | ||
| 804 | }); | 827 | }); |
| 805 | const feed: ThreadFeedEntry[] = [ | 828 | const feed: ThreadFeedEntry[] = [ |
| 806 | { | 829 | { |
| ... | @@ -818,26 +841,216 @@ describe("buildThreadFeed", () => { | ... | @@ -818,26 +841,216 @@ describe("buildThreadFeed", () => { |
| 818 | ]; | 841 | ]; |
| 819 | 842 | ||
| 820 | const collapsed = deriveThreadFeedPresentation(feed, null, new Set()); | 843 | const collapsed = deriveThreadFeedPresentation(feed, null, new Set()); |
| 821 | expect(collapsed.map((entry) => entry.id)).toEqual(["activity-3", "work-toggle:work-group-1"]); | 844 | expect(collapsed.map((entry) => entry.id)).toEqual(["work-toggle:work-group:activity-1"]); |
| 822 | expect(collapsed[1]).toMatchObject({ | 845 | expect(collapsed[0]).toMatchObject({ |
| 823 | type: "work-toggle", | 846 | type: "work-toggle", |
| 824 | groupId: "work-group-1", | 847 | groupId: "work-group:activity-1", |
| 825 | hiddenCount: 2, | 848 | hiddenCount: 3, |
| 826 | expanded: false, | 849 | expanded: false, |
| 850 | summary: "Ran 3 commands", | ||
| 827 | }); | 851 | }); |
| 828 | 852 | ||
| 829 | const expanded = deriveThreadFeedPresentation(feed, null, new Set(), new Set(["work-group-1"])); | 853 | const expanded = deriveThreadFeedPresentation( |
| 854 | feed, | ||
| 855 | null, | ||
| 856 | new Set(), | ||
| 857 | new Set(["work-group:activity-1"]), | ||
| 858 | ); | ||
| 830 | expect(expanded.map((entry) => entry.id)).toEqual([ | 859 | expect(expanded.map((entry) => entry.id)).toEqual([ |
| 860 | "work-toggle:work-group:activity-1", | ||
| 831 | "activity-1", | 861 | "activity-1", |
| 832 | "activity-2", | 862 | "activity-2", |
| 833 | "activity-3", | 863 | "activity-3", |
| 834 | "work-toggle:work-group-1", | ||
| 835 | ]); | 864 | ]); |
| 836 | expect(expanded.at(-1)).toMatchObject({ | 865 | expect(expanded[0]).toMatchObject({ |
| 837 | type: "work-toggle", | 866 | type: "work-toggle", |
| 838 | expanded: true, | 867 | expanded: true, |
| 839 | }); | 868 | }); |
| 840 | }); | 869 | }); |
| 870 | |||
| 871 | it("keeps live state on the active uninterrupted tool run", () => { | ||
| 872 | const turnId = TurnId.make("turn-live-tools"); | ||
| 873 | const activity = ( | ||
| 874 | id: string, | ||
| 875 | status: ThreadFeedActivity["status"], | ||
| 876 | lifecycleStatus: ThreadFeedActivity["lifecycleStatus"], | ||
| 877 | tone: "tool" | "error" = "tool", | ||
| 878 | command?: string, | ||
| 879 | ): ThreadFeedActivity => ({ | ||
| 880 | id, | ||
| 881 | createdAt: `2026-04-01T00:00:0${id.at(-1)}.000Z`, | ||
| 882 | turnId, | ||
| 883 | summary: `Tool ${id}`, | ||
| 884 | detail: null, | ||
| 885 | canExpand: false, | ||
| 886 | getFullDetail: () => null, | ||
| 887 | getCopyText: () => id, | ||
| 888 | icon: "command", | ||
| 889 | toolLike: true, | ||
| 890 | status, | ||
| 891 | lifecycleStatus, | ||
| 892 | workEntry: { | ||
| 893 | id, | ||
| 894 | createdAt: `2026-04-01T00:00:0${id.at(-1)}.000Z`, | ||
| 895 | turnId, | ||
| 896 | label: `Tool ${id}`, | ||
| 897 | tone, | ||
| 898 | toolLifecycleStatus: lifecycleStatus, | ||
| 899 | ...(command ? { command, itemType: "command_execution" as const } : {}), | ||
| 900 | }, | ||
| 901 | }); | ||
| 902 | const feed: ThreadFeedEntry[] = [ | ||
| 903 | { | ||
| 904 | type: "activity-group", | ||
| 905 | id: "activity-1", | ||
| 906 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 907 | turnId, | ||
| 908 | activities: [ | ||
| 909 | activity("activity-1", "success", "completed"), | ||
| 910 | activity("activity-2", "failure", "failed", "error"), | ||
| 911 | activity("activity-3", "success", "completed", "tool", "sudo -u root pnpm test"), | ||
| 912 | ], | ||
| 913 | }, | ||
| 914 | ]; | ||
| 915 | const latestTurn = { | ||
| 916 | turnId, | ||
| 917 | state: "running" as const, | ||
| 918 | requestedAt: "2026-04-01T00:00:00.000Z", | ||
| 919 | startedAt: "2026-04-01T00:00:00.000Z", | ||
| 920 | completedAt: null, | ||
| 921 | assistantMessageId: null, | ||
| 922 | }; | ||
| 923 | |||
| 924 | const rows = deriveThreadFeedPresentation( | ||
| 925 | feed, | ||
| 926 | latestTurn, | ||
| 927 | new Set(), | ||
| 928 | new Set(), | ||
| 929 | latestTurn.startedAt, | ||
| 930 | ); | ||
| 931 | expect(rows.slice(0, 3).map((entry) => [entry.id, entry.type])).toEqual([ | ||
| 932 | ["work-toggle:work-group:activity-1", "work-toggle"], | ||
| 933 | ["activity-2", "activity-group"], | ||
| 934 | ["work-live:work-group:activity-3", "work-toggle"], | ||
| 935 | ]); | ||
| 936 | expect(rows.slice(0, 3).map((entry) => entry.type === "work-toggle" && entry.live)).toEqual([ | ||
| 937 | false, | ||
| 938 | false, | ||
| 939 | true, | ||
| 940 | ]); | ||
| 941 | expect(rows[2]).toMatchObject({ | ||
| 942 | summary: "Running pnpm", | ||
| 943 | summaryKind: "command", | ||
| 944 | live: true, | ||
| 945 | shimmer: false, | ||
| 946 | }); | ||
| 947 | }); | ||
| 948 | |||
| 949 | it("does not revive cached in-progress tools after work stops", () => { | ||
| 950 | const turnId = TurnId.make("turn-stale-tool"); | ||
| 951 | const feed: ThreadFeedEntry[] = [ | ||
| 952 | { | ||
| 953 | type: "activity-group", | ||
| 954 | id: "stale-tool", | ||
| 955 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 956 | turnId, | ||
| 957 | activities: [ | ||
| 958 | { | ||
| 959 | id: "stale-tool", | ||
| 960 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 961 | turnId, | ||
| 962 | summary: "Running tests", | ||
| 963 | detail: null, | ||
| 964 | canExpand: false, | ||
| 965 | getFullDetail: () => null, | ||
| 966 | getCopyText: () => "", | ||
| 967 | icon: "command", | ||
| 968 | toolLike: true, | ||
| 969 | status: "neutral", | ||
| 970 | lifecycleStatus: "inProgress", | ||
| 971 | workEntry: { | ||
| 972 | id: "stale-tool", | ||
| 973 | createdAt: "2026-04-01T00:00:01.000Z", | ||
| 974 | turnId, | ||
| 975 | label: "Running tests", | ||
| 976 | tone: "tool", | ||
| 977 | toolLifecycleStatus: "inProgress", | ||
| 978 | }, | ||
| 979 | }, | ||
| 980 | ], | ||
| 981 | }, | ||
| 982 | ]; | ||
| 983 | const latestTurn = { | ||
| 984 | turnId, | ||
| 985 | state: "running" as const, | ||
| 986 | requestedAt: "2026-04-01T00:00:00.000Z", | ||
| 987 | startedAt: "2026-04-01T00:00:00.000Z", | ||
| 988 | completedAt: null, | ||
| 989 | assistantMessageId: null, | ||
| 990 | }; | ||
| 991 | |||
| 992 | expect(deriveThreadFeedPresentation(feed, latestTurn, new Set())).toEqual([]); | ||
| 993 | expect( | ||
| 994 | deriveThreadFeedPresentation(feed, latestTurn, new Set(), new Set(), latestTurn.startedAt), | ||
| 995 | ).toMatchObject([{ type: "work-toggle", live: true, shimmer: true }]); | ||
| 996 | }); | ||
| 997 | |||
| 998 | it("collapses interleaved tool lifecycles by call identity", () => { | ||
| 999 | const turnId = TurnId.make("turn-parallel-tools"); | ||
| 1000 | const toolActivity = ( | ||
| 1001 | id: string, | ||
| 1002 | toolCallId: string, | ||
| 1003 | kind: "tool.updated" | "tool.completed", | ||
| 1004 | status: "inProgress" | "completed", | ||
| 1005 | detail: string, | ||
| 1006 | nestedId = false, | ||
| 1007 | ) => | ||
| 1008 | makeActivity({ | ||
| 1009 | id: EventId.make(id), | ||
| 1010 | kind, | ||
| 1011 | tone: "tool", | ||
| 1012 | summary: `Run ${toolCallId} command`, | ||
| 1013 | createdAt: `2026-04-01T00:00:0${id.at(-1)}.000Z`, | ||
| 1014 | turnId, | ||
| 1015 | payload: { | ||
| 1016 | ...(nestedId ? { data: { toolCallId } } : { toolCallId }), | ||
| 1017 | itemType: "command_execution", | ||
| 1018 | status, | ||
| 1019 | detail, | ||
| 1020 | }, | ||
| 1021 | }); | ||
| 1022 | const thread = makeThread({ | ||
| 1023 | id: ThreadId.make("thread-parallel-tools"), | ||
| 1024 | projectId: ProjectId.make("project-1"), | ||
| 1025 | title: "Parallel tools", | ||
| 1026 | activities: [ | ||
| 1027 | toolActivity("call-a-1", "call-a", "tool.updated", "inProgress", "starting"), | ||
| 1028 | toolActivity("call-b-2", "call-b", "tool.updated", "inProgress", "starting", true), | ||
| 1029 | toolActivity("call-a-3", "call-a", "tool.completed", "completed", "first output"), | ||
| 1030 | toolActivity("call-b-4", "call-b", "tool.completed", "completed", "second output", true), | ||
| 1031 | ], | ||
| 1032 | }); | ||
| 1033 | |||
| 1034 | const feed = buildThreadFeed(thread); | ||
| 1035 | const activityGroup = feed.find((entry) => entry.type === "activity-group"); | ||
| 1036 | expect(activityGroup).toMatchObject({ | ||
| 1037 | type: "activity-group", | ||
| 1038 | activities: [ | ||
| 1039 | { id: "call-a-1", lifecycleStatus: "completed", detail: "first output" }, | ||
| 1040 | { id: "call-b-2", lifecycleStatus: "completed", detail: "second output" }, | ||
| 1041 | ], | ||
| 1042 | }); | ||
| 1043 | expect( | ||
| 1044 | deriveThreadFeedPresentation(feed, null, new Set([turnId])).find( | ||
| 1045 | (entry) => entry.type === "work-toggle", | ||
| 1046 | ), | ||
| 1047 | ).toMatchObject({ | ||
| 1048 | type: "work-toggle", | ||
| 1049 | hiddenCount: 2, | ||
| 1050 | summary: "Ran 2 commands", | ||
| 1051 | live: false, | ||
| 1052 | }); | ||
| 1053 | }); | ||
| 841 | }); | 1054 | }); |
| 842 | 1055 | ||
| 843 | describe("quiet timeline: nested agents", () => { | 1056 | describe("quiet timeline: nested agents", () => { |
| ... | @@ -873,5 +1086,8 @@ describe("quiet timeline: nested agents", () => { | ... | @@ -873,5 +1086,8 @@ describe("quiet timeline: nested agents", () => { |
| 873 | ); | 1086 | ); |
| 874 | expect(ids).toContain("nested-done"); | 1087 | expect(ids).toContain("nested-done"); |
| 875 | expect(ids).not.toContain("shell-done"); | 1088 | expect(ids).not.toContain("shell-done"); |
| 1089 | expect(deriveThreadFeedPresentation(feed, null, new Set())).toMatchObject([ | ||
| 1090 | { type: "activity-group", id: "nested-done" }, | ||
| 1091 | ]); | ||
| 876 | }); | 1092 | }); |
| 877 | }); | 1093 | }); |
apps/mobile/src/lib/threadActivity.ts+234-59| ... | @@ -13,6 +13,14 @@ import type { | ... | @@ -13,6 +13,14 @@ import type { |
| 13 | UserInputQuestion, | 13 | UserInputQuestion, |
| 14 | } from "@t3tools/contracts"; | 14 | } from "@t3tools/contracts"; |
| 15 | import { formatDuration } from "@t3tools/shared/orchestrationTiming"; | 15 | import { formatDuration } from "@t3tools/shared/orchestrationTiming"; |
| 16 | import { | ||
| 17 | normalizeCompactToolLabel, | ||
| 18 | omitSupersededLifecycleMarkers, | ||
| 19 | summarizeToolGroup, | ||
| 20 | toolGroupSummaryKind, | ||
| 21 | type ToolGroupSummaryKind, | ||
| 22 | } from "@t3tools/client-runtime/work-log/presentation"; | ||
| 23 | import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label"; | ||
| 16 | 24 | ||
| 17 | import * as Arr from "effect/Array"; | 25 | import * as Arr from "effect/Array"; |
| 18 | import * as Order from "effect/Order"; | 26 | import * as Order from "effect/Order"; |
| ... | @@ -65,13 +73,15 @@ export interface ThreadFeedActivity { | ... | @@ -65,13 +73,15 @@ export interface ThreadFeedActivity { |
| 65 | | "zap"; | 73 | | "zap"; |
| 66 | readonly toolLike: boolean; | 74 | readonly toolLike: boolean; |
| 67 | readonly status: "success" | "failure" | "neutral" | null; | 75 | readonly status: "success" | "failure" | "neutral" | null; |
| 76 | readonly lifecycleStatus?: WorkLogToolLifecycleStatus; | ||
| 77 | readonly workEntry: WorkLogEntry; | ||
| 78 | readonly groupedToolDetail?: boolean; | ||
| 79 | readonly live?: boolean; | ||
| 68 | } | 80 | } |
| 69 | 81 | ||
| 70 | const MAX_VISIBLE_WORK_LOG_ENTRIES = 1; | ||
| 71 | |||
| 72 | type WorkLogToolLifecycleStatus = "inProgress" | "completed" | "failed" | "declined" | "stopped"; | 82 | type WorkLogToolLifecycleStatus = "inProgress" | "completed" | "failed" | "declined" | "stopped"; |
| 73 | 83 | ||
| 74 | interface WorkLogEntry { | 84 | export interface WorkLogEntry { |
| 75 | id: string; | 85 | id: string; |
| 76 | createdAt: string; | 86 | createdAt: string; |
| 77 | turnId: TurnId | null; | 87 | turnId: TurnId | null; |
| ... | @@ -85,11 +95,14 @@ interface WorkLogEntry { | ... | @@ -85,11 +95,14 @@ interface WorkLogEntry { |
| 85 | itemType?: ToolLifecycleItemType; | 95 | itemType?: ToolLifecycleItemType; |
| 86 | requestKind?: PendingApproval["requestKind"]; | 96 | requestKind?: PendingApproval["requestKind"]; |
| 87 | toolLifecycleStatus?: WorkLogToolLifecycleStatus; | 97 | toolLifecycleStatus?: WorkLogToolLifecycleStatus; |
| 98 | sourceActivityKind?: OrchestrationThreadActivity["kind"]; | ||
| 99 | toolCallId?: string; | ||
| 100 | agentSpawn?: boolean; | ||
| 88 | toolData?: unknown; | 101 | toolData?: unknown; |
| 89 | } | 102 | } |
| 90 | 103 | ||
| 91 | interface DerivedWorkLogEntry extends WorkLogEntry { | 104 | interface DerivedWorkLogEntry extends WorkLogEntry { |
| 92 | activityKind: OrchestrationThreadActivity["kind"]; | 105 | sourceActivityKind: OrchestrationThreadActivity["kind"]; |
| 93 | collapseKey?: string; | 106 | collapseKey?: string; |
| 94 | /** Grouping key for subagent lifecycle rows (one row per agent). */ | 107 | /** Grouping key for subagent lifecycle rows (one row per agent). */ |
| 95 | taskId?: string; | 108 | taskId?: string; |
| ... | @@ -112,11 +125,6 @@ type RawThreadFeedEntry = | ... | @@ -112,11 +125,6 @@ type RawThreadFeedEntry = |
| 112 | 125 | ||
| 113 | export type ThreadFeedEntry = | 126 | export type ThreadFeedEntry = |
| 114 | | Extract<RawThreadFeedEntry, { type: "message" }> | 127 | | Extract<RawThreadFeedEntry, { type: "message" }> |
| 115 | | { | ||
| 116 | readonly type: "working"; | ||
| 117 | readonly id: string; | ||
| 118 | readonly createdAt: string; | ||
| 119 | } | ||
| 120 | | { | 128 | | { |
| 121 | readonly type: "activity-group"; | 129 | readonly type: "activity-group"; |
| 122 | readonly id: string; | 130 | readonly id: string; |
| ... | @@ -132,7 +140,11 @@ export type ThreadFeedEntry = | ... | @@ -132,7 +140,11 @@ export type ThreadFeedEntry = |
| 132 | readonly groupId: string; | 140 | readonly groupId: string; |
| 133 | readonly hiddenCount: number; | 141 | readonly hiddenCount: number; |
| 134 | readonly expanded: boolean; | 142 | readonly expanded: boolean; |
| 135 | readonly onlyToolActivities: boolean; | 143 | readonly summary: string; |
| 144 | readonly summaryKind: ToolGroupSummaryKind; | ||
| 145 | readonly hasFailure: boolean; | ||
| 146 | readonly live: boolean; | ||
| 147 | readonly shimmer: boolean; | ||
| 136 | } | 148 | } |
| 137 | | { | 149 | | { |
| 138 | readonly type: "turn-fold"; | 150 | readonly type: "turn-fold"; |
| ... | @@ -412,8 +424,16 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo | ... | @@ -412,8 +424,16 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo |
| 412 | : activity.tone === "approval" | 424 | : activity.tone === "approval" |
| 413 | ? "info" | 425 | ? "info" |
| 414 | : activity.tone, | 426 | : activity.tone, |
| 415 | activityKind: activity.kind, | 427 | sourceActivityKind: activity.kind, |
| 416 | }; | 428 | }; |
| 429 | const toolCallId = | ||
| 430 | asTrimmedString(payload?.toolCallId) ?? asTrimmedString(asRecord(payload?.data)?.toolCallId); | ||
| 431 | if (toolCallId) { | ||
| 432 | entry.toolCallId = toolCallId; | ||
| 433 | } | ||
| 434 | if (isTaskActivity && payload?.agentKind === "agent") { | ||
| 435 | entry.agentSpawn = true; | ||
| 436 | } | ||
| 417 | const itemType = extractWorkLogItemType(payload); | 437 | const itemType = extractWorkLogItemType(payload); |
| 418 | const requestKind = extractWorkLogRequestKind(payload); | 438 | const requestKind = extractWorkLogRequestKind(payload); |
| 419 | if ( | 439 | if ( |
| ... | @@ -472,12 +492,13 @@ function collapseDerivedWorkLogEntries( | ... | @@ -472,12 +492,13 @@ function collapseDerivedWorkLogEntries( |
| 472 | // Subagent rows collapse by identity, not adjacency (quiet-timeline | 492 | // Subagent rows collapse by identity, not adjacency (quiet-timeline |
| 473 | // guarantee; mirrors web's session-logic). | 493 | // guarantee; mirrors web's session-logic). |
| 474 | const taskRowIndex = new Map<string, number>(); | 494 | const taskRowIndex = new Map<string, number>(); |
| 495 | const toolLifecycleRowIndex = new Map<string, number>(); | ||
| 475 | for (const entry of entries) { | 496 | for (const entry of entries) { |
| 476 | const isTaskRow = | 497 | const isTaskRow = |
| 477 | entry.taskId !== undefined && | 498 | entry.taskId !== undefined && |
| 478 | (entry.activityKind === "task.progress" || | 499 | (entry.sourceActivityKind === "task.progress" || |
| 479 | entry.activityKind === "task.completed" || | 500 | entry.sourceActivityKind === "task.completed" || |
| 480 | entry.activityKind === "task.updated"); | 501 | entry.sourceActivityKind === "task.updated"); |
| 481 | if (isTaskRow && entry.taskId !== undefined) { | 502 | if (isTaskRow && entry.taskId !== undefined) { |
| 482 | const existingIndex = taskRowIndex.get(entry.taskId); | 503 | const existingIndex = taskRowIndex.get(entry.taskId); |
| 483 | if (existingIndex !== undefined) { | 504 | if (existingIndex !== undefined) { |
| ... | @@ -488,30 +509,78 @@ function collapseDerivedWorkLogEntries( | ... | @@ -488,30 +509,78 @@ function collapseDerivedWorkLogEntries( |
| 488 | collapsed.push(entry); | 509 | collapsed.push(entry); |
| 489 | continue; | 510 | continue; |
| 490 | } | 511 | } |
| 512 | const lifecycleKey = toolLifecycleCollapseMapKey(entry); | ||
| 513 | if (lifecycleKey !== undefined) { | ||
| 514 | const matchingIndex = toolLifecycleRowIndex.get(lifecycleKey); | ||
| 515 | const matchingEntry = matchingIndex === undefined ? undefined : collapsed[matchingIndex]; | ||
| 516 | if ( | ||
| 517 | matchingIndex !== undefined && | ||
| 518 | matchingEntry && | ||
| 519 | shouldCollapseToolLifecycleEntries(matchingEntry, entry) | ||
| 520 | ) { | ||
| 521 | collapsed[matchingIndex] = mergeDerivedWorkLogEntries(matchingEntry, entry); | ||
| 522 | continue; | ||
| 523 | } | ||
| 524 | toolLifecycleRowIndex.delete(lifecycleKey); | ||
| 525 | } | ||
| 491 | const previous = collapsed.at(-1); | 526 | const previous = collapsed.at(-1); |
| 492 | if (previous && shouldCollapseToolLifecycleEntries(previous, entry)) { | 527 | if (previous && shouldCollapseToolLifecycleEntries(previous, entry)) { |
| 493 | collapsed[collapsed.length - 1] = mergeDerivedWorkLogEntries(previous, entry); | 528 | const previousIndex = collapsed.length - 1; |
| 529 | const previousKey = toolLifecycleCollapseMapKey(previous); | ||
| 530 | if (previousKey !== undefined) toolLifecycleRowIndex.delete(previousKey); | ||
| 531 | const merged = mergeDerivedWorkLogEntries(previous, entry); | ||
| 532 | collapsed[previousIndex] = merged; | ||
| 533 | const mergedKey = toolLifecycleCollapseMapKey(merged); | ||
| 534 | if (mergedKey !== undefined) toolLifecycleRowIndex.set(mergedKey, previousIndex); | ||
| 494 | continue; | 535 | continue; |
| 495 | } | 536 | } |
| 496 | collapsed.push(entry); | 537 | collapsed.push(entry); |
| 538 | if (lifecycleKey !== undefined) { | ||
| 539 | toolLifecycleRowIndex.set(lifecycleKey, collapsed.length - 1); | ||
| 540 | } | ||
| 497 | } | 541 | } |
| 498 | return collapsed; | 542 | return collapsed; |
| 499 | } | 543 | } |
| 500 | 544 | ||
| 545 | function toolLifecycleCollapseMapKey(entry: DerivedWorkLogEntry): string | undefined { | ||
| 546 | if ( | ||
| 547 | entry.sourceActivityKind !== "tool.updated" && | ||
| 548 | entry.sourceActivityKind !== "tool.completed" | ||
| 549 | ) { | ||
| 550 | return undefined; | ||
| 551 | } | ||
| 552 | return entry.toolCallId ? `tool:${entry.turnId ?? "no-turn"}:${entry.toolCallId}` : undefined; | ||
| 553 | } | ||
| 554 | |||
| 501 | function shouldCollapseToolLifecycleEntries( | 555 | function shouldCollapseToolLifecycleEntries( |
| 502 | previous: DerivedWorkLogEntry, | 556 | previous: DerivedWorkLogEntry, |
| 503 | next: DerivedWorkLogEntry, | 557 | next: DerivedWorkLogEntry, |
| 504 | ): boolean { | 558 | ): boolean { |
| 505 | if (previous.activityKind !== "tool.updated" && previous.activityKind !== "tool.completed") { | 559 | if ( |
| 560 | previous.sourceActivityKind !== "tool.updated" && | ||
| 561 | previous.sourceActivityKind !== "tool.completed" | ||
| 562 | ) { | ||
| 506 | return false; | 563 | return false; |
| 507 | } | 564 | } |
| 508 | if (next.activityKind !== "tool.updated" && next.activityKind !== "tool.completed") { | 565 | if (next.sourceActivityKind !== "tool.updated" && next.sourceActivityKind !== "tool.completed") { |
| 509 | return false; | 566 | return false; |
| 510 | } | 567 | } |
| 511 | if (previous.activityKind === "tool.completed") { | 568 | if (previous.turnId !== next.turnId) { |
| 512 | return false; | 569 | return false; |
| 513 | } | 570 | } |
| 514 | return previous.collapseKey !== undefined && previous.collapseKey === next.collapseKey; | 571 | if (previous.sourceActivityKind === "tool.completed") { |
| 572 | return false; | ||
| 573 | } | ||
| 574 | if (previous.collapseKey !== undefined && previous.collapseKey === next.collapseKey) { | ||
| 575 | return true; | ||
| 576 | } | ||
| 577 | return ( | ||
| 578 | previous.toolCallId !== undefined && | ||
| 579 | next.toolCallId === undefined && | ||
| 580 | previous.itemType === next.itemType && | ||
| 581 | normalizeCompactToolLabel(previous.toolTitle ?? previous.label) === | ||
| 582 | normalizeCompactToolLabel(next.toolTitle ?? next.label) | ||
| 583 | ); | ||
| 515 | } | 584 | } |
| 516 | 585 | ||
| 517 | function mergeDerivedWorkLogEntries( | 586 | function mergeDerivedWorkLogEntries( |
| ... | @@ -527,10 +596,13 @@ function mergeDerivedWorkLogEntries( | ... | @@ -527,10 +596,13 @@ function mergeDerivedWorkLogEntries( |
| 527 | const requestKind = next.requestKind ?? previous.requestKind; | 596 | const requestKind = next.requestKind ?? previous.requestKind; |
| 528 | const collapseKey = next.collapseKey ?? previous.collapseKey; | 597 | const collapseKey = next.collapseKey ?? previous.collapseKey; |
| 529 | const toolLifecycleStatus = next.toolLifecycleStatus ?? previous.toolLifecycleStatus; | 598 | const toolLifecycleStatus = next.toolLifecycleStatus ?? previous.toolLifecycleStatus; |
| 599 | const toolCallId = next.toolCallId ?? previous.toolCallId; | ||
| 530 | const toolData = next.toolData ?? previous.toolData; | 600 | const toolData = next.toolData ?? previous.toolData; |
| 531 | return { | 601 | return { |
| 532 | ...previous, | 602 | ...previous, |
| 533 | ...next, | 603 | ...next, |
| 604 | id: previous.id, | ||
| 605 | createdAt: previous.createdAt, | ||
| 534 | ...(detail ? { detail } : {}), | 606 | ...(detail ? { detail } : {}), |
| 535 | ...(command ? { command } : {}), | 607 | ...(command ? { command } : {}), |
| 536 | ...(rawCommand ? { rawCommand } : {}), | 608 | ...(rawCommand ? { rawCommand } : {}), |
| ... | @@ -540,6 +612,7 @@ function mergeDerivedWorkLogEntries( | ... | @@ -540,6 +612,7 @@ function mergeDerivedWorkLogEntries( |
| 540 | ...(requestKind ? { requestKind } : {}), | 612 | ...(requestKind ? { requestKind } : {}), |
| 541 | ...(collapseKey ? { collapseKey } : {}), | 613 | ...(collapseKey ? { collapseKey } : {}), |
| 542 | ...(toolLifecycleStatus ? { toolLifecycleStatus } : {}), | 614 | ...(toolLifecycleStatus ? { toolLifecycleStatus } : {}), |
| 615 | ...(toolCallId ? { toolCallId } : {}), | ||
| 543 | ...(toolData !== undefined ? { toolData } : {}), | 616 | ...(toolData !== undefined ? { toolData } : {}), |
| 544 | }; | 617 | }; |
| 545 | } | 618 | } |
| ... | @@ -556,9 +629,15 @@ function mergeChangedFiles( | ... | @@ -556,9 +629,15 @@ function mergeChangedFiles( |
| 556 | } | 629 | } |
| 557 | 630 | ||
| 558 | function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | undefined { | 631 | function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | undefined { |
| 559 | if (entry.activityKind !== "tool.updated" && entry.activityKind !== "tool.completed") { | 632 | if ( |
| 633 | entry.sourceActivityKind !== "tool.updated" && | ||
| 634 | entry.sourceActivityKind !== "tool.completed" | ||
| 635 | ) { | ||
| 560 | return undefined; | 636 | return undefined; |
| 561 | } | 637 | } |
| 638 | if (entry.toolCallId) { | ||
| 639 | return `tool:${entry.turnId ?? "no-turn"}:${entry.toolCallId}`; | ||
| 640 | } | ||
| 562 | const normalizedLabel = normalizeCompactToolLabel(entry.toolTitle ?? entry.label); | 641 | const normalizedLabel = normalizeCompactToolLabel(entry.toolTitle ?? entry.label); |
| 563 | const detail = entry.detail?.trim() ?? ""; | 642 | const detail = entry.detail?.trim() ?? ""; |
| 564 | const itemType = entry.itemType ?? ""; | 643 | const itemType = entry.itemType ?? ""; |
| ... | @@ -568,10 +647,6 @@ function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | un | ... | @@ -568,10 +647,6 @@ function deriveToolLifecycleCollapseKey(entry: DerivedWorkLogEntry): string | un |
| 568 | return [itemType, normalizedLabel, detail].join("\u001f"); | 647 | return [itemType, normalizedLabel, detail].join("\u001f"); |
| 569 | } | 648 | } |
| 570 | 649 | ||
| 571 | function normalizeCompactToolLabel(value: string): string { | ||
| 572 | return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim(); | ||
| 573 | } | ||
| 574 | |||
| 575 | function workLogEntryIsToolLike(entry: WorkLogEntry): boolean { | 650 | function workLogEntryIsToolLike(entry: WorkLogEntry): boolean { |
| 576 | if (entry.tone === "tool" || entry.tone === "thinking" || entry.tone === "error") { | 651 | if (entry.tone === "tool" || entry.tone === "thinking" || entry.tone === "error") { |
| 577 | return true; | 652 | return true; |
| ... | @@ -648,12 +723,12 @@ function workEntryStatus(entry: WorkLogEntry): ThreadFeedActivity["status"] { | ... | @@ -648,12 +723,12 @@ function workEntryStatus(entry: WorkLogEntry): ThreadFeedActivity["status"] { |
| 648 | 723 | ||
| 649 | function workEntryIcon(entry: DerivedWorkLogEntry): ThreadFeedActivity["icon"] { | 724 | function workEntryIcon(entry: DerivedWorkLogEntry): ThreadFeedActivity["icon"] { |
| 650 | if ( | 725 | if ( |
| 651 | entry.activityKind === "user-input.requested" || | 726 | entry.sourceActivityKind === "user-input.requested" || |
| 652 | entry.activityKind === "user-input.resolved" | 727 | entry.sourceActivityKind === "user-input.resolved" |
| 653 | ) { | 728 | ) { |
| 654 | return "message"; | 729 | return "message"; |
| 655 | } | 730 | } |
| 656 | if (entry.activityKind === "runtime.warning") return "warning"; | 731 | if (entry.sourceActivityKind === "runtime.warning") return "warning"; |
| 657 | if (entry.requestKind === "command") return "command"; | 732 | if (entry.requestKind === "command") return "command"; |
| 658 | if (entry.requestKind === "file-read") return "eye"; | 733 | if (entry.requestKind === "file-read") return "eye"; |
| 659 | if (entry.requestKind === "file-change") return "edit"; | 734 | if (entry.requestKind === "file-change") return "edit"; |
| ... | @@ -1287,10 +1362,14 @@ export function deriveThreadFeedPresentation( | ... | @@ -1287,10 +1362,14 @@ export function deriveThreadFeedPresentation( |
| 1287 | activeWorkStartedAt: string | null = null, | 1362 | activeWorkStartedAt: string | null = null, |
| 1288 | ): ThreadFeedEntry[] { | 1363 | ): ThreadFeedEntry[] { |
| 1289 | const sourceFeed = feed.filter( | 1364 | const sourceFeed = feed.filter( |
| 1290 | (entry) => | 1365 | (entry) => entry.type !== "turn-fold" && entry.type !== "work-toggle", |
| 1291 | entry.type !== "turn-fold" && entry.type !== "work-toggle" && entry.type !== "working", | 1366 | ); |
| 1367 | const activeTailGroup = sourceFeed.findLast( | ||
| 1368 | (entry) => entry.type !== "message" || !isEmptyMessage(entry), | ||
| 1292 | ); | 1369 | ); |
| 1293 | const foldsByAnchorId = deriveThreadFeedTurnFolds(sourceFeed, latestTurn); | 1370 | const foldsByAnchorId = deriveThreadFeedTurnFolds(sourceFeed, latestTurn); |
| 1371 | const unsettledTurnId = deriveUnsettledTurnId(latestTurn); | ||
| 1372 | const isWorking = activeWorkStartedAt !== null; | ||
| 1294 | const collapsedEntryIds = new Set<string>(); | 1373 | const collapsedEntryIds = new Set<string>(); |
| 1295 | for (const fold of foldsByAnchorId.values()) { | 1374 | for (const fold of foldsByAnchorId.values()) { |
| 1296 | if (!expandedTurnIds.has(fold.turnId)) { | 1375 | if (!expandedTurnIds.has(fold.turnId)) { |
| ... | @@ -1302,6 +1381,13 @@ export function deriveThreadFeedPresentation( | ... | @@ -1302,6 +1381,13 @@ export function deriveThreadFeedPresentation( |
| 1302 | 1381 | ||
| 1303 | const result: ThreadFeedEntry[] = []; | 1382 | const result: ThreadFeedEntry[] = []; |
| 1304 | for (const entry of sourceFeed) { | 1383 | for (const entry of sourceFeed) { |
| 1384 | const isActiveTailGroup = | ||
| 1385 | isWorking && | ||
| 1386 | unsettledTurnId !== null && | ||
| 1387 | entry.type === "activity-group" && | ||
| 1388 | activeTailGroup?.type === "activity-group" && | ||
| 1389 | activeTailGroup.id === entry.id && | ||
| 1390 | entry.turnId === unsettledTurnId; | ||
| 1305 | const fold = foldsByAnchorId.get(entry.id); | 1391 | const fold = foldsByAnchorId.get(entry.id); |
| 1306 | if (fold) { | 1392 | if (fold) { |
| 1307 | result.push({ | 1393 | result.push({ |
| ... | @@ -1314,49 +1400,65 @@ export function deriveThreadFeedPresentation( | ... | @@ -1314,49 +1400,65 @@ export function deriveThreadFeedPresentation( |
| 1314 | }); | 1400 | }); |
| 1315 | } | 1401 | } |
| 1316 | if (!collapsedEntryIds.has(entry.id)) { | 1402 | if (!collapsedEntryIds.has(entry.id)) { |
| 1317 | appendPresentedFeedEntry(result, entry, expandedWorkGroupIds); | 1403 | appendPresentedFeedEntry( |
| 1404 | result, | ||
| 1405 | entry, | ||
| 1406 | expandedWorkGroupIds, | ||
| 1407 | unsettledTurnId, | ||
| 1408 | isWorking, | ||
| 1409 | isActiveTailGroup, | ||
| 1410 | ); | ||
| 1318 | } | 1411 | } |
| 1319 | } | 1412 | } |
| 1320 | if (activeWorkStartedAt !== null) { | ||
| 1321 | result.push({ | ||
| 1322 | type: "working", | ||
| 1323 | id: "working-indicator-row", | ||
| 1324 | createdAt: activeWorkStartedAt, | ||
| 1325 | }); | ||
| 1326 | } | ||
| 1327 | return result; | 1413 | return result; |
| 1328 | } | 1414 | } |
| 1329 | 1415 | ||
| 1330 | function appendPresentedFeedEntry( | 1416 | function appendPresentedFeedEntry( |
| 1331 | result: ThreadFeedEntry[], | 1417 | result: ThreadFeedEntry[], |
| 1332 | entry: Exclude<ThreadFeedEntry, { readonly type: "turn-fold" | "work-toggle" | "working" }>, | 1418 | entry: Exclude<ThreadFeedEntry, { readonly type: "turn-fold" | "work-toggle" }>, |
| 1333 | expandedWorkGroupIds: ReadonlySet<string>, | 1419 | expandedWorkGroupIds: ReadonlySet<string>, |
| 1420 | unsettledTurnId: TurnId | null, | ||
| 1421 | isWorking: boolean, | ||
| 1422 | activeTail: boolean, | ||
| 1334 | ): void { | 1423 | ): void { |
| 1335 | if (entry.type !== "activity-group") { | 1424 | if (entry.type !== "activity-group") { |
| 1336 | result.push(entry); | 1425 | result.push(entry); |
| 1337 | return; | 1426 | return; |
| 1338 | } | 1427 | } |
| 1339 | 1428 | ||
| 1340 | const activities = entry.activities.filter( | 1429 | const activities = omitSupersededLifecycleMarkers( |
| 1341 | (activity) => !(activity.toolLike && activity.status === "neutral"), | 1430 | entry.activities.filter( |
| 1431 | (activity) => | ||
| 1432 | !(activity.toolLike && activity.status === "neutral") || | ||
| 1433 | (isWorking && | ||
| 1434 | activity.lifecycleStatus === "inProgress" && | ||
| 1435 | activity.turnId === unsettledTurnId), | ||
| 1436 | ), | ||
| 1437 | (activity) => activity.workEntry, | ||
| 1342 | ); | 1438 | ); |
| 1343 | if (activities.length === 0) { | 1439 | if (activities.length === 0) { |
| 1344 | return; | 1440 | return; |
| 1345 | } | 1441 | } |
| 1346 | if (activities.length <= MAX_VISIBLE_WORK_LOG_ENTRIES) { | 1442 | let groupableRun: ThreadFeedActivity[] = []; |
| 1347 | result.push({ | 1443 | const flushGroupableRun = (isTrailingRun: boolean) => { |
| 1348 | ...entry, | 1444 | if (groupableRun.length === 0) return; |
| 1349 | activities, | 1445 | appendToolGroupRows( |
| 1350 | }); | 1446 | result, |
| 1351 | return; | 1447 | entry, |
| 1352 | } | 1448 | groupableRun, |
| 1353 | 1449 | expandedWorkGroupIds, | |
| 1354 | const groupId = entry.id; | 1450 | unsettledTurnId, |
| 1355 | const expanded = expandedWorkGroupIds.has(groupId); | 1451 | isWorking, |
| 1356 | const hiddenCount = activities.length - MAX_VISIBLE_WORK_LOG_ENTRIES; | 1452 | activeTail && isTrailingRun, |
| 1357 | const visibleActivities = expanded ? activities : activities.slice(-MAX_VISIBLE_WORK_LOG_ENTRIES); | 1453 | ); |
| 1358 | 1454 | groupableRun = []; | |
| 1359 | for (const activity of visibleActivities) { | 1455 | }; |
| 1456 | for (const activity of activities) { | ||
| 1457 | if (activity.workEntry.tone !== "error" && activity.workEntry.agentSpawn !== true) { | ||
| 1458 | groupableRun.push(activity); | ||
| 1459 | continue; | ||
| 1460 | } | ||
| 1461 | flushGroupableRun(false); | ||
| 1360 | result.push({ | 1462 | result.push({ |
| 1361 | type: "activity-group", | 1463 | type: "activity-group", |
| 1362 | id: activity.id, | 1464 | id: activity.id, |
| ... | @@ -1365,16 +1467,87 @@ function appendPresentedFeedEntry( | ... | @@ -1365,16 +1467,87 @@ function appendPresentedFeedEntry( |
| 1365 | activities: [activity], | 1467 | activities: [activity], |
| 1366 | }); | 1468 | }); |
| 1367 | } | 1469 | } |
| 1470 | flushGroupableRun(true); | ||
| 1471 | } | ||
| 1472 | |||
| 1473 | function appendToolGroupRows( | ||
| 1474 | result: ThreadFeedEntry[], | ||
| 1475 | sourceGroup: Extract<ThreadFeedEntry, { readonly type: "activity-group" }>, | ||
| 1476 | activities: ReadonlyArray<ThreadFeedActivity>, | ||
| 1477 | expandedWorkGroupIds: ReadonlySet<string>, | ||
| 1478 | unsettledTurnId: TurnId | null, | ||
| 1479 | isWorking: boolean, | ||
| 1480 | activeTail: boolean, | ||
| 1481 | ): void { | ||
| 1482 | const firstEntry = activities[0]!.workEntry; | ||
| 1483 | const identity = firstEntry.toolCallId | ||
| 1484 | ? `tool:${firstEntry.turnId ?? "no-turn"}:${firstEntry.toolCallId}` | ||
| 1485 | : activities[0]!.id; | ||
| 1486 | const groupId = `work-group:${identity}`; | ||
| 1487 | const expanded = expandedWorkGroupIds.has(groupId); | ||
| 1488 | const latestInProgressActivity = activities.findLast( | ||
| 1489 | (activity) => | ||
| 1490 | isWorking && activity.lifecycleStatus === "inProgress" && activity.turnId === unsettledTurnId, | ||
| 1491 | ); | ||
| 1492 | const live = activeTail || latestInProgressActivity !== undefined; | ||
| 1493 | const latestActivity = activeTail | ||
| 1494 | ? activities.at(-1)! | ||
| 1495 | : (latestInProgressActivity ?? activities.at(-1)!); | ||
| 1496 | const summary = live | ||
| 1497 | ? liveToolActivitySummary(latestActivity) | ||
| 1498 | : activities.length === 1 && !activities[0]!.toolLike | ||
| 1499 | ? activities[0]!.workEntry.label | ||
| 1500 | : summarizeToolGroup(activities.map((activity) => activity.workEntry)); | ||
| 1368 | result.push({ | 1501 | result.push({ |
| 1369 | type: "work-toggle", | 1502 | type: "work-toggle", |
| 1370 | id: `work-toggle:${groupId}`, | 1503 | id: `${live ? "work-live" : "work-toggle"}:${groupId}`, |
| 1371 | createdAt: entry.createdAt, | 1504 | createdAt: sourceGroup.createdAt, |
| 1372 | turnId: entry.turnId, | 1505 | turnId: sourceGroup.turnId, |
| 1373 | groupId, | 1506 | groupId, |
| 1374 | hiddenCount, | 1507 | hiddenCount: activities.length, |
| 1375 | expanded, | 1508 | expanded, |
| 1376 | onlyToolActivities: activities.every((activity) => activity.toolLike), | 1509 | summary, |
| 1510 | summaryKind: toolGroupSummaryKind( | ||
| 1511 | (live ? [latestActivity] : activities).map((activity) => activity.workEntry), | ||
| 1512 | ), | ||
| 1513 | hasFailure: activities.findLast((activity) => activity.toolLike)?.status === "failure", | ||
| 1514 | live, | ||
| 1515 | shimmer: | ||
| 1516 | isWorking && | ||
| 1517 | latestActivity.lifecycleStatus === "inProgress" && | ||
| 1518 | latestActivity.turnId === unsettledTurnId, | ||
| 1377 | }); | 1519 | }); |
| 1520 | if (!expanded) { | ||
| 1521 | return; | ||
| 1522 | } | ||
| 1523 | for (const activity of activities) { | ||
| 1524 | result.push({ | ||
| 1525 | type: "activity-group", | ||
| 1526 | id: activity.id, | ||
| 1527 | createdAt: activity.createdAt, | ||
| 1528 | turnId: activity.turnId, | ||
| 1529 | activities: [ | ||
| 1530 | { | ||
| 1531 | ...activity, | ||
| 1532 | groupedToolDetail: true, | ||
| 1533 | live: | ||
| 1534 | isWorking && | ||
| 1535 | activity.id === latestActivity.id && | ||
| 1536 | activity.lifecycleStatus === "inProgress" && | ||
| 1537 | activity.turnId === unsettledTurnId, | ||
| 1538 | }, | ||
| 1539 | ], | ||
| 1540 | }); | ||
| 1541 | } | ||
| 1542 | } | ||
| 1543 | |||
| 1544 | function liveToolActivitySummary(activity: ThreadFeedActivity): string { | ||
| 1545 | const command = activity.workEntry.command?.trim(); | ||
| 1546 | if (command) { | ||
| 1547 | const program = commandProgramName(command); | ||
| 1548 | return program ? `Running ${program}` : "Running command"; | ||
| 1549 | } | ||
| 1550 | return activity.detail ?? activity.summary; | ||
| 1378 | } | 1551 | } |
| 1379 | 1552 | ||
| 1380 | /** | 1553 | /** |
| ... | @@ -1609,6 +1782,8 @@ export function buildThreadFeed( | ... | @@ -1609,6 +1782,8 @@ export function buildThreadFeed( |
| 1609 | icon: workEntryIcon(entry), | 1782 | icon: workEntryIcon(entry), |
| 1610 | toolLike: workLogEntryIsToolLike(entry), | 1783 | toolLike: workLogEntryIsToolLike(entry), |
| 1611 | status: workEntryStatus(entry), | 1784 | status: workEntryStatus(entry), |
| 1785 | ...(entry.toolLifecycleStatus ? { lifecycleStatus: entry.toolLifecycleStatus } : {}), | ||
| 1786 | workEntry: entry, | ||
| 1612 | }, | 1787 | }, |
| 1613 | }; | 1788 | }; |
| 1614 | }), | 1789 | }), |
apps/web/src/components/chat/MessagesTimeline.tsx+1-170| ... | @@ -7,6 +7,7 @@ import { | ... | @@ -7,6 +7,7 @@ import { |
| 7 | type TurnId, | 7 | type TurnId, |
| 8 | } from "@t3tools/contracts"; | 8 | } from "@t3tools/contracts"; |
| 9 | import { parseScopedThreadKey } from "@t3tools/client-runtime/environment"; | 9 | import { parseScopedThreadKey } from "@t3tools/client-runtime/environment"; |
| 10 | import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label"; | ||
| 10 | import type { AgentPanelModel } from "@t3tools/client-runtime/state/subagentRuntime"; | 11 | import type { AgentPanelModel } from "@t3tools/client-runtime/state/subagentRuntime"; |
| 11 | import { | 12 | import { |
| 12 | emptyAgentPanelModel, | 13 | emptyAgentPanelModel, |
| ... | @@ -2289,176 +2290,6 @@ function workEntryRawCommand( | ... | @@ -2289,176 +2290,6 @@ function workEntryRawCommand( |
| 2289 | return rawCommand === workEntry.command.trim() ? null : rawCommand; | 2290 | return rawCommand === workEntry.command.trim() ? null : rawCommand; |
| 2290 | } | 2291 | } |
| 2291 | 2292 | ||
| 2292 | type CommandWrapper = "env" | "sudo"; | ||
| 2293 | |||
| 2294 | const COMMAND_WRAPPER_OPTIONS_WITH_VALUE: Record<CommandWrapper, ReadonlySet<string>> = { | ||
| 2295 | env: new Set(["-C", "--chdir", "-S", "--split-string", "-u", "--unset"]), | ||
| 2296 | sudo: new Set(["-C", "--close-from", "-D", "--chdir", "-g", "--group", "-u", "--user"]), | ||
| 2297 | }; | ||
| 2298 | |||
| 2299 | const COMMAND_WRAPPER_FLAGS: Record<CommandWrapper, ReadonlySet<string>> = { | ||
| 2300 | env: new Set(["-0", "--null", "-i", "--ignore-environment", "--debug", "-v"]), | ||
| 2301 | sudo: new Set(["-A", "--askpass", "-b", "--background", "-E", "-H", "-i", "-n", "-S"]), | ||
| 2302 | }; | ||
| 2303 | |||
| 2304 | function tokenizeShellCommand(command: string): string[] | null { | ||
| 2305 | const input = command.trim(); | ||
| 2306 | const tokens: string[] = []; | ||
| 2307 | let current = ""; | ||
| 2308 | let quote: '"' | "'" | null = null; | ||
| 2309 | let escaping = false; | ||
| 2310 | let substitutionDepth = 0; | ||
| 2311 | let tokenStarted = false; | ||
| 2312 | |||
| 2313 | for (let index = 0; index < input.length; index += 1) { | ||
| 2314 | const character = input[index]!; | ||
| 2315 | if (escaping) { | ||
| 2316 | current += character; | ||
| 2317 | escaping = false; | ||
| 2318 | tokenStarted = true; | ||
| 2319 | continue; | ||
| 2320 | } | ||
| 2321 | if (character === "\\" && quote !== "'") { | ||
| 2322 | const nextCharacter = input[index + 1]; | ||
| 2323 | const isWindowsDrivePath = quote === null && /^[A-Za-z]:/.test(current); | ||
| 2324 | if ( | ||
| 2325 | (quote === '"' || isWindowsDrivePath) && | ||
| 2326 | nextCharacter !== undefined && | ||
| 2327 | nextCharacter !== '"' && | ||
| 2328 | nextCharacter !== "\\" && | ||
| 2329 | nextCharacter !== "$" && | ||
| 2330 | nextCharacter !== "`" && | ||
| 2331 | nextCharacter !== "\n" | ||
| 2332 | ) { | ||
| 2333 | current += character; | ||
| 2334 | tokenStarted = true; | ||
| 2335 | continue; | ||
| 2336 | } | ||
| 2337 | escaping = true; | ||
| 2338 | tokenStarted = true; | ||
| 2339 | continue; | ||
| 2340 | } | ||
| 2341 | if (quote !== null) { | ||
| 2342 | if (character === quote) { | ||
| 2343 | quote = null; | ||
| 2344 | } else { | ||
| 2345 | current += character; | ||
| 2346 | } | ||
| 2347 | tokenStarted = true; | ||
| 2348 | continue; | ||
| 2349 | } | ||
| 2350 | if (character === "$" && input[index + 1] === "(") { | ||
| 2351 | current += "$("; | ||
| 2352 | substitutionDepth += 1; | ||
| 2353 | tokenStarted = true; | ||
| 2354 | index += 1; | ||
| 2355 | continue; | ||
| 2356 | } | ||
| 2357 | if (character === ")" && substitutionDepth > 0) { | ||
| 2358 | current += character; | ||
| 2359 | substitutionDepth -= 1; | ||
| 2360 | tokenStarted = true; | ||
| 2361 | continue; | ||
| 2362 | } | ||
| 2363 | if (character === '"' || character === "'") { | ||
| 2364 | quote = character; | ||
| 2365 | tokenStarted = true; | ||
| 2366 | continue; | ||
| 2367 | } | ||
| 2368 | if (/\s/u.test(character)) { | ||
| 2369 | if (substitutionDepth > 0) { | ||
| 2370 | current += character; | ||
| 2371 | tokenStarted = true; | ||
| 2372 | continue; | ||
| 2373 | } | ||
| 2374 | if (tokenStarted) { | ||
| 2375 | tokens.push(current); | ||
| 2376 | current = ""; | ||
| 2377 | tokenStarted = false; | ||
| 2378 | } | ||
| 2379 | continue; | ||
| 2380 | } | ||
| 2381 | current += character; | ||
| 2382 | tokenStarted = true; | ||
| 2383 | } | ||
| 2384 | |||
| 2385 | if (quote !== null || escaping || substitutionDepth > 0) return null; | ||
| 2386 | if (tokenStarted) tokens.push(current); | ||
| 2387 | return tokens; | ||
| 2388 | } | ||
| 2389 | |||
| 2390 | function commandProgramName(command: string, depth = 0): string | null { | ||
| 2391 | if (depth >= 8) return null; | ||
| 2392 | const tokens = tokenizeShellCommand(command); | ||
| 2393 | if (tokens === null) return null; | ||
| 2394 | let index = 0; | ||
| 2395 | let wrapper: CommandWrapper | null = null; | ||
| 2396 | |||
| 2397 | while (index < tokens.length) { | ||
| 2398 | const token = tokens[index]; | ||
| 2399 | if (!token) return null; | ||
| 2400 | if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) { | ||
| 2401 | index += 1; | ||
| 2402 | continue; | ||
| 2403 | } | ||
| 2404 | const tokenProgram = token.split(/[\\/]/).at(-1); | ||
| 2405 | if (tokenProgram === "env" || tokenProgram === "sudo") { | ||
| 2406 | wrapper = tokenProgram; | ||
| 2407 | index += 1; | ||
| 2408 | continue; | ||
| 2409 | } | ||
| 2410 | if (wrapper !== null && token === "--") { | ||
| 2411 | wrapper = null; | ||
| 2412 | index += 1; | ||
| 2413 | continue; | ||
| 2414 | } | ||
| 2415 | if (wrapper !== null && token.startsWith("-")) { | ||
| 2416 | if (wrapper === "env" && (token === "-S" || token === "--split-string")) { | ||
| 2417 | const splitCommand = tokens[index + 1]; | ||
| 2418 | return splitCommand ? commandProgramName(splitCommand, depth + 1) : null; | ||
| 2419 | } | ||
| 2420 | if (wrapper === "env" && token.startsWith("--split-string=")) { | ||
| 2421 | return commandProgramName(token.slice("--split-string=".length), depth + 1); | ||
| 2422 | } | ||
| 2423 | if (COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(token)) { | ||
| 2424 | if (tokens[index + 1] === undefined) return null; | ||
| 2425 | index += 2; | ||
| 2426 | continue; | ||
| 2427 | } | ||
| 2428 | if (COMMAND_WRAPPER_FLAGS[wrapper].has(token)) { | ||
| 2429 | index += 1; | ||
| 2430 | continue; | ||
| 2431 | } | ||
| 2432 | const equalsIndex = token.indexOf("="); | ||
| 2433 | if (token.startsWith("--") && equalsIndex > 2) { | ||
| 2434 | if (!COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(token.slice(0, equalsIndex))) { | ||
| 2435 | return null; | ||
| 2436 | } | ||
| 2437 | index += 1; | ||
| 2438 | continue; | ||
| 2439 | } | ||
| 2440 | if (/^-[A-Za-z].+/.test(token) && !token.startsWith("--")) { | ||
| 2441 | let consumesNextToken = false; | ||
| 2442 | for (const [optionIndex, option] of token.slice(1).split("").entries()) { | ||
| 2443 | const shortOption = `-${option}`; | ||
| 2444 | if (COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(shortOption)) { | ||
| 2445 | consumesNextToken = optionIndex === token.length - 2; | ||
| 2446 | break; | ||
| 2447 | } | ||
| 2448 | if (!COMMAND_WRAPPER_FLAGS[wrapper].has(shortOption)) return null; | ||
| 2449 | } | ||
| 2450 | if (consumesNextToken && tokens[index + 1] === undefined) return null; | ||
| 2451 | index += consumesNextToken ? 2 : 1; | ||
| 2452 | continue; | ||
| 2453 | } | ||
| 2454 | return null; | ||
| 2455 | } | ||
| 2456 | return token.split(/[\\/]/).at(-1) || null; | ||
| 2457 | } | ||
| 2458 | |||
| 2459 | return null; | ||
| 2460 | } | ||
| 2461 | |||
| 2462 | function liveWorkEntryLabel( | 2293 | function liveWorkEntryLabel( |
| 2463 | workEntry: TimelineWorkEntry, | 2294 | workEntry: TimelineWorkEntry, |
| 2464 | workspaceRoot: string | undefined, | 2295 | workspaceRoot: string | undefined, |
packages/client-runtime/package.json+8| ... | @@ -158,6 +158,14 @@ | ... | @@ -158,6 +158,14 @@ |
| 158 | "./state/vcs": { | 158 | "./state/vcs": { |
| 159 | "types": "./src/state/vcs.ts", | 159 | "types": "./src/state/vcs.ts", |
| 160 | "default": "./src/state/vcs.ts" | 160 | "default": "./src/state/vcs.ts" |
| 161 | }, | ||
| 162 | "./work-log/presentation": { | ||
| 163 | "types": "./src/work-log/presentation.ts", | ||
| 164 | "default": "./src/work-log/presentation.ts" | ||
| 165 | }, | ||
| 166 | "./work-log/command-label": { | ||
| 167 | "types": "./src/work-log/commandLabel.ts", | ||
| 168 | "default": "./src/work-log/commandLabel.ts" | ||
| 161 | } | 169 | } |
| 162 | }, | 170 | }, |
| 163 | "scripts": { | 171 | "scripts": { |
packages/client-runtime/src/work-log/commandLabel.ts created+169| ... | @@ -0,0 +1,169 @@ | ||
| 1 | type CommandWrapper = "env" | "sudo"; | ||
| 2 | |||
| 3 | const COMMAND_WRAPPER_OPTIONS_WITH_VALUE: Record<CommandWrapper, ReadonlySet<string>> = { | ||
| 4 | env: new Set(["-C", "--chdir", "-S", "--split-string", "-u", "--unset"]), | ||
| 5 | sudo: new Set(["-C", "--close-from", "-D", "--chdir", "-g", "--group", "-u", "--user"]), | ||
| 6 | }; | ||
| 7 | |||
| 8 | const COMMAND_WRAPPER_FLAGS: Record<CommandWrapper, ReadonlySet<string>> = { | ||
| 9 | env: new Set(["-0", "--null", "-i", "--ignore-environment", "--debug", "-v"]), | ||
| 10 | sudo: new Set(["-A", "--askpass", "-b", "--background", "-E", "-H", "-i", "-n", "-S"]), | ||
| 11 | }; | ||
| 12 | |||
| 13 | function tokenizeShellCommand(command: string): string[] | null { | ||
| 14 | const input = command.trim(); | ||
| 15 | const tokens: string[] = []; | ||
| 16 | let current = ""; | ||
| 17 | let quote: '"' | "'" | null = null; | ||
| 18 | let escaping = false; | ||
| 19 | let substitutionDepth = 0; | ||
| 20 | let tokenStarted = false; | ||
| 21 | |||
| 22 | for (let index = 0; index < input.length; index += 1) { | ||
| 23 | const character = input[index]!; | ||
| 24 | if (escaping) { | ||
| 25 | current += character; | ||
| 26 | escaping = false; | ||
| 27 | tokenStarted = true; | ||
| 28 | continue; | ||
| 29 | } | ||
| 30 | if (character === "\\" && quote !== "'") { | ||
| 31 | const nextCharacter = input[index + 1]; | ||
| 32 | const isWindowsDrivePath = quote === null && /^[A-Za-z]:/.test(current); | ||
| 33 | if ( | ||
| 34 | (quote === '"' || isWindowsDrivePath) && | ||
| 35 | nextCharacter !== undefined && | ||
| 36 | nextCharacter !== '"' && | ||
| 37 | nextCharacter !== "\\" && | ||
| 38 | nextCharacter !== "$" && | ||
| 39 | nextCharacter !== "`" && | ||
| 40 | nextCharacter !== "\n" | ||
| 41 | ) { | ||
| 42 | current += character; | ||
| 43 | tokenStarted = true; | ||
| 44 | continue; | ||
| 45 | } | ||
| 46 | escaping = true; | ||
| 47 | tokenStarted = true; | ||
| 48 | continue; | ||
| 49 | } | ||
| 50 | if (quote !== null) { | ||
| 51 | if (character === quote) { | ||
| 52 | quote = null; | ||
| 53 | } else { | ||
| 54 | current += character; | ||
| 55 | } | ||
| 56 | tokenStarted = true; | ||
| 57 | continue; | ||
| 58 | } | ||
| 59 | if (character === "$" && input[index + 1] === "(") { | ||
| 60 | current += "$("; | ||
| 61 | substitutionDepth += 1; | ||
| 62 | tokenStarted = true; | ||
| 63 | index += 1; | ||
| 64 | continue; | ||
| 65 | } | ||
| 66 | if (character === ")" && substitutionDepth > 0) { | ||
| 67 | current += character; | ||
| 68 | substitutionDepth -= 1; | ||
| 69 | tokenStarted = true; | ||
| 70 | continue; | ||
| 71 | } | ||
| 72 | if (character === '"' || character === "'") { | ||
| 73 | quote = character; | ||
| 74 | tokenStarted = true; | ||
| 75 | continue; | ||
| 76 | } | ||
| 77 | if (/\s/u.test(character)) { | ||
| 78 | if (substitutionDepth > 0) { | ||
| 79 | current += character; | ||
| 80 | tokenStarted = true; | ||
| 81 | continue; | ||
| 82 | } | ||
| 83 | if (tokenStarted) { | ||
| 84 | tokens.push(current); | ||
| 85 | current = ""; | ||
| 86 | tokenStarted = false; | ||
| 87 | } | ||
| 88 | continue; | ||
| 89 | } | ||
| 90 | current += character; | ||
| 91 | tokenStarted = true; | ||
| 92 | } | ||
| 93 | |||
| 94 | if (quote !== null || escaping || substitutionDepth > 0) return null; | ||
| 95 | if (tokenStarted) tokens.push(current); | ||
| 96 | return tokens; | ||
| 97 | } | ||
| 98 | |||
| 99 | export function commandProgramName(command: string, depth = 0): string | null { | ||
| 100 | if (depth >= 8) return null; | ||
| 101 | const tokens = tokenizeShellCommand(command); | ||
| 102 | if (tokens === null) return null; | ||
| 103 | let index = 0; | ||
| 104 | let wrapper: CommandWrapper | null = null; | ||
| 105 | |||
| 106 | while (index < tokens.length) { | ||
| 107 | const token = tokens[index]; | ||
| 108 | if (!token) return null; | ||
| 109 | if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) { | ||
| 110 | index += 1; | ||
| 111 | continue; | ||
| 112 | } | ||
| 113 | const tokenProgram = token.split(/[\\/]/).at(-1); | ||
| 114 | if (tokenProgram === "env" || tokenProgram === "sudo") { | ||
| 115 | wrapper = tokenProgram; | ||
| 116 | index += 1; | ||
| 117 | continue; | ||
| 118 | } | ||
| 119 | if (wrapper !== null && token === "--") { | ||
| 120 | wrapper = null; | ||
| 121 | index += 1; | ||
| 122 | continue; | ||
| 123 | } | ||
| 124 | if (wrapper !== null && token.startsWith("-")) { | ||
| 125 | if (wrapper === "env" && (token === "-S" || token === "--split-string")) { | ||
| 126 | const splitCommand = tokens[index + 1]; | ||
| 127 | return splitCommand ? commandProgramName(splitCommand, depth + 1) : null; | ||
| 128 | } | ||
| 129 | if (wrapper === "env" && token.startsWith("--split-string=")) { | ||
| 130 | return commandProgramName(token.slice("--split-string=".length), depth + 1); | ||
| 131 | } | ||
| 132 | if (COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(token)) { | ||
| 133 | if (tokens[index + 1] === undefined) return null; | ||
| 134 | index += 2; | ||
| 135 | continue; | ||
| 136 | } | ||
| 137 | if (COMMAND_WRAPPER_FLAGS[wrapper].has(token)) { | ||
| 138 | index += 1; | ||
| 139 | continue; | ||
| 140 | } | ||
| 141 | const equalsIndex = token.indexOf("="); | ||
| 142 | if (token.startsWith("--") && equalsIndex > 2) { | ||
| 143 | if (!COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(token.slice(0, equalsIndex))) { | ||
| 144 | return null; | ||
| 145 | } | ||
| 146 | index += 1; | ||
| 147 | continue; | ||
| 148 | } | ||
| 149 | if (/^-[A-Za-z].+/.test(token) && !token.startsWith("--")) { | ||
| 150 | let consumesNextToken = false; | ||
| 151 | for (const [optionIndex, option] of token.slice(1).split("").entries()) { | ||
| 152 | const shortOption = `-${option}`; | ||
| 153 | if (COMMAND_WRAPPER_OPTIONS_WITH_VALUE[wrapper].has(shortOption)) { | ||
| 154 | consumesNextToken = optionIndex === token.length - 2; | ||
| 155 | break; | ||
| 156 | } | ||
| 157 | if (!COMMAND_WRAPPER_FLAGS[wrapper].has(shortOption)) return null; | ||
| 158 | } | ||
| 159 | if (consumesNextToken && tokens[index + 1] === undefined) return null; | ||
| 160 | index += consumesNextToken ? 2 : 1; | ||
| 161 | continue; | ||
| 162 | } | ||
| 163 | return null; | ||
| 164 | } | ||
| 165 | return token.split(/[\\/]/).at(-1) || null; | ||
| 166 | } | ||
| 167 | |||
| 168 | return null; | ||
| 169 | } | ||
packages/client-runtime/src/work-log/presentation.ts created+188| ... | @@ -0,0 +1,188 @@ | ||
| 1 | import { isToolLifecycleItemType, type ToolLifecycleItemType } from "@t3tools/contracts"; | ||
| 2 | |||
| 3 | export interface WorkLogPresentationEntry { | ||
| 4 | readonly label: string; | ||
| 5 | readonly toolTitle?: string; | ||
| 6 | readonly tone: "thinking" | "tool" | "info" | "error"; | ||
| 7 | readonly command?: string; | ||
| 8 | readonly changedFiles?: ReadonlyArray<string>; | ||
| 9 | readonly itemType?: ToolLifecycleItemType; | ||
| 10 | readonly requestKind?: string; | ||
| 11 | readonly turnId?: string | null; | ||
| 12 | readonly toolCallId?: string; | ||
| 13 | readonly toolLifecycleStatus?: string; | ||
| 14 | readonly sourceActivityKind?: string; | ||
| 15 | readonly taskId?: string; | ||
| 16 | } | ||
| 17 | |||
| 18 | export type ToolGroupAction = | ||
| 19 | | "read" | ||
| 20 | | "edit" | ||
| 21 | | "command" | ||
| 22 | | "code-search" | ||
| 23 | | "search" | ||
| 24 | | "other" | ||
| 25 | | "update"; | ||
| 26 | |||
| 27 | export type ToolGroupSummaryKind = | ||
| 28 | | ToolGroupAction | ||
| 29 | | "dynamic-tool" | ||
| 30 | | "agent-tool" | ||
| 31 | | "tone-tool" | ||
| 32 | | "mixed"; | ||
| 33 | |||
| 34 | export function normalizeCompactToolLabel(value: string): string { | ||
| 35 | return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim(); | ||
| 36 | } | ||
| 37 | |||
| 38 | function workLogEntryIsToolLike(entry: WorkLogPresentationEntry): boolean { | ||
| 39 | if (entry.tone === "tool" || entry.tone === "thinking" || entry.tone === "error") return true; | ||
| 40 | if (entry.command !== undefined && entry.command.trim().length > 0) return true; | ||
| 41 | if (entry.requestKind !== undefined) return true; | ||
| 42 | return entry.itemType !== undefined && isToolLifecycleItemType(entry.itemType); | ||
| 43 | } | ||
| 44 | |||
| 45 | export function workLogEntryIsLocalCodeSearch(entry: WorkLogPresentationEntry): boolean { | ||
| 46 | return ( | ||
| 47 | entry.itemType === "web_search" && | ||
| 48 | /\bgrep\b/i.test(normalizeCompactToolLabel(entry.toolTitle ?? entry.label)) | ||
| 49 | ); | ||
| 50 | } | ||
| 51 | |||
| 52 | export function toolGroupAction(entry: WorkLogPresentationEntry): ToolGroupAction { | ||
| 53 | if ( | ||
| 54 | entry.requestKind === "file-read" || | ||
| 55 | entry.itemType === "image_view" || | ||
| 56 | (entry.itemType === "dynamic_tool_call" && entry.toolTitle === "Read File") | ||
| 57 | ) { | ||
| 58 | return "read"; | ||
| 59 | } | ||
| 60 | if ( | ||
| 61 | entry.requestKind === "file-change" || | ||
| 62 | entry.itemType === "file_change" || | ||
| 63 | (entry.changedFiles?.length ?? 0) > 0 | ||
| 64 | ) { | ||
| 65 | return "edit"; | ||
| 66 | } | ||
| 67 | if (entry.requestKind === "command" || entry.itemType === "command_execution" || entry.command) { | ||
| 68 | return "command"; | ||
| 69 | } | ||
| 70 | if (workLogEntryIsLocalCodeSearch(entry)) return "code-search"; | ||
| 71 | if (entry.itemType === "web_search") return "search"; | ||
| 72 | return workLogEntryIsToolLike(entry) ? "other" : "update"; | ||
| 73 | } | ||
| 74 | |||
| 75 | function toolGroupActionCount( | ||
| 76 | action: ToolGroupAction, | ||
| 77 | entries: ReadonlyArray<WorkLogPresentationEntry>, | ||
| 78 | ): number { | ||
| 79 | if (action !== "edit") return entries.length; | ||
| 80 | |||
| 81 | const changedFiles = new Set<string>(); | ||
| 82 | let editsWithoutFileDetails = 0; | ||
| 83 | for (const entry of entries) { | ||
| 84 | if (!entry.changedFiles || entry.changedFiles.length === 0) { | ||
| 85 | editsWithoutFileDetails += 1; | ||
| 86 | continue; | ||
| 87 | } | ||
| 88 | for (const file of entry.changedFiles) changedFiles.add(file); | ||
| 89 | } | ||
| 90 | return changedFiles.size + editsWithoutFileDetails; | ||
| 91 | } | ||
| 92 | |||
| 93 | function toolGroupActionLabel(action: ToolGroupAction, count: number): string { | ||
| 94 | switch (action) { | ||
| 95 | case "read": | ||
| 96 | return `Read ${count} ${count === 1 ? "file" : "files"}`; | ||
| 97 | case "edit": | ||
| 98 | return `Changed ${count} ${count === 1 ? "file" : "files"}`; | ||
| 99 | case "command": | ||
| 100 | return `Ran ${count} ${count === 1 ? "command" : "commands"}`; | ||
| 101 | case "search": | ||
| 102 | return `Searched the web ${count} ${count === 1 ? "time" : "times"}`; | ||
| 103 | case "code-search": | ||
| 104 | return `Searched code ${count} ${count === 1 ? "time" : "times"}`; | ||
| 105 | case "other": | ||
| 106 | return `Used ${count} ${count === 1 ? "tool" : "tools"}`; | ||
| 107 | case "update": | ||
| 108 | return `Received ${count} ${count === 1 ? "update" : "updates"}`; | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | export function summarizeToolGroup(entries: ReadonlyArray<WorkLogPresentationEntry>): string { | ||
| 113 | const summaryEntries = omitSupersededLifecycleMarkers(entries, (entry) => entry); | ||
| 114 | const groupedEntries = new Map<ToolGroupAction, WorkLogPresentationEntry[]>(); | ||
| 115 | for (const entry of summaryEntries) { | ||
| 116 | const action = toolGroupAction(entry); | ||
| 117 | const group = groupedEntries.get(action); | ||
| 118 | if (group) group.push(entry); | ||
| 119 | else groupedEntries.set(action, [entry]); | ||
| 120 | } | ||
| 121 | const labels = [...groupedEntries].map(([action, actionEntries]) => | ||
| 122 | toolGroupActionLabel(action, toolGroupActionCount(action, actionEntries)), | ||
| 123 | ); | ||
| 124 | const sentenceLabels = labels.map((label, index) => | ||
| 125 | index === 0 ? label : label.charAt(0).toLowerCase() + label.slice(1), | ||
| 126 | ); | ||
| 127 | if (sentenceLabels.length < 2) return sentenceLabels[0] ?? ""; | ||
| 128 | if (sentenceLabels.length === 2) return sentenceLabels.join(" and "); | ||
| 129 | return `${sentenceLabels.slice(0, -1).join(", ")}, and ${sentenceLabels.at(-1)}`; | ||
| 130 | } | ||
| 131 | |||
| 132 | export function omitSupersededLifecycleMarkers<T>( | ||
| 133 | entries: readonly T[], | ||
| 134 | workEntryFor: (entry: T) => WorkLogPresentationEntry, | ||
| 135 | ): T[] { | ||
| 136 | const laterTerminalIdentities = new Set<string>(); | ||
| 137 | const reversedEntries: T[] = []; | ||
| 138 | |||
| 139 | for (let index = entries.length - 1; index >= 0; index -= 1) { | ||
| 140 | const entry = entries[index]!; | ||
| 141 | const workEntry = workEntryFor(entry); | ||
| 142 | const normalizedLabel = normalizeCompactToolLabel(workEntry.toolTitle ?? workEntry.label); | ||
| 143 | const identity = [ | ||
| 144 | workEntry.turnId ?? "no-turn", | ||
| 145 | workEntry.itemType ?? "", | ||
| 146 | normalizedLabel, | ||
| 147 | ].join("\u001f"); | ||
| 148 | const activityKind = workEntry.sourceActivityKind; | ||
| 149 | const isStatuslessIdlessMarker = | ||
| 150 | workEntry.toolCallId === undefined && | ||
| 151 | workEntry.toolLifecycleStatus === undefined && | ||
| 152 | (activityKind === "tool.started" || activityKind === "tool.updated"); | ||
| 153 | if (isStatuslessIdlessMarker && laterTerminalIdentities.has(identity)) continue; | ||
| 154 | |||
| 155 | reversedEntries.push(entry); | ||
| 156 | if ( | ||
| 157 | activityKind === "tool.completed" || | ||
| 158 | (workEntry.toolLifecycleStatus !== undefined && | ||
| 159 | workEntry.toolLifecycleStatus !== "inProgress") | ||
| 160 | ) { | ||
| 161 | laterTerminalIdentities.add(identity); | ||
| 162 | } | ||
| 163 | } | ||
| 164 | |||
| 165 | return reversedEntries.toReversed(); | ||
| 166 | } | ||
| 167 | |||
| 168 | export function toolGroupSummaryKind( | ||
| 169 | entries: ReadonlyArray<WorkLogPresentationEntry>, | ||
| 170 | ): ToolGroupSummaryKind { | ||
| 171 | const actions = new Set(entries.map(toolGroupAction)); | ||
| 172 | if (actions.size !== 1) return "mixed"; | ||
| 173 | |||
| 174 | const action = actions.values().next().value!; | ||
| 175 | if (action !== "other") return action; | ||
| 176 | |||
| 177 | const fallbackKinds = new Set( | ||
| 178 | entries.map((entry): ToolGroupSummaryKind => { | ||
| 179 | if (entry.itemType === "mcp_tool_call") return "other"; | ||
| 180 | if (entry.itemType === "dynamic_tool_call") return "dynamic-tool"; | ||
| 181 | if (entry.itemType === "collab_agent_tool_call" || entry.taskId) return "agent-tool"; | ||
| 182 | if (entry.tone === "thinking") return "agent-tool"; | ||
| 183 | if (entry.tone === "tool") return "tone-tool"; | ||
| 184 | return "other"; | ||
| 185 | }), | ||
| 186 | ); | ||
| 187 | return fallbackKinds.size === 1 ? fallbackKinds.values().next().value! : "mixed"; | ||
| 188 | } | ||
patches/@legendapp__list@3.3.5.patch+696-82| ... | @@ -1,5 +1,5 @@ | ... | @@ -1,5 +1,5 @@ |
| 1 | diff --git a/keyboard.d.ts b/keyboard.d.ts | 1 | diff --git a/keyboard.d.ts b/keyboard.d.ts |
| 2 | index 367945cdfa8a8c260b7a127657a75c016c9ab46f..95263268a47d3f1f57bbc5d528bcc77674f9121f 100644 | 2 | index 367945cdfa8a8c260b7a127657a75c016c9ab46f..0ac7bf8b8e9c386058b2374199e58fe8c92d39b3 100644 |
| 3 | --- a/keyboard.d.ts | 3 | --- a/keyboard.d.ts |
| 4 | +++ b/keyboard.d.ts | 4 | +++ b/keyboard.d.ts |
| 5 | @@ -279,7 +279,7 @@ type KeyboardChatComposerInsetListRef = { | 5 | @@ -279,7 +279,7 @@ type KeyboardChatComposerInsetListRef = { |
| ... | @@ -7,7 +7,7 @@ index 367945cdfa8a8c260b7a127657a75c016c9ab46f..95263268a47d3f1f57bbc5d528bcc776 | ... | @@ -7,7 +7,7 @@ index 367945cdfa8a8c260b7a127657a75c016c9ab46f..95263268a47d3f1f57bbc5d528bcc776 |
| 7 | current: Pick<View, "measure"> | null; | 7 | current: Pick<View, "measure"> | null; |
| 8 | }; | 8 | }; |
| 9 | -declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number): { | 9 | -declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number): { |
| 10 | +declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number, heightAdjustment?: number): { | 10 | +declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number, heightAdjustment?: number, animationDuration?: number): { |
| 11 | contentInsetEndAdjustment: SharedValue<number>; | 11 | contentInsetEndAdjustment: SharedValue<number>; |
| 12 | onComposerLayout: (event: LayoutChangeEvent) => void; | 12 | onComposerLayout: (event: LayoutChangeEvent) => void; |
| 13 | }; | 13 | }; |
| ... | @@ -23,15 +23,15 @@ index 367945cdfa8a8c260b7a127657a75c016c9ab46f..95263268a47d3f1f57bbc5d528bcc776 | ... | @@ -23,15 +23,15 @@ index 367945cdfa8a8c260b7a127657a75c016c9ab46f..95263268a47d3f1f57bbc5d528bcc776 |
| 23 | } & React.RefAttributes<LegendListRef>) => React.ReactElement | null; | 23 | } & React.RefAttributes<LegendListRef>) => React.ReactElement | null; |
| 24 | 24 | ||
| 25 | diff --git a/keyboard.js b/keyboard.js | 25 | diff --git a/keyboard.js b/keyboard.js |
| 26 | index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600ccb8d3be62 100644 | 26 | index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..321c4c305129dd3ce3f06127c47d51720a73fb83 100644 |
| 27 | --- a/keyboard.js | 27 | --- a/keyboard.js |
| 28 | +++ b/keyboard.js | 28 | +++ b/keyboard.js |
| 29 | @@ -33,19 +33,19 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !reactNativeKeyboardController. | 29 | @@ -33,19 +33,22 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !reactNativeKeyboardController. |
| 30 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." | 30 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." |
| 31 | ); | 31 | ); |
| 32 | } | 32 | } |
| 33 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { | 33 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { |
| 34 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0) { | 34 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0, animationDuration = 0) { |
| 35 | const contentInsetEndAdjustment = reactNativeReanimated.useSharedValue(initialHeight); | 35 | const contentInsetEndAdjustment = reactNativeReanimated.useSharedValue(initialHeight); |
| 36 | const lastHeightRef = React.useRef(void 0); | 36 | const lastHeightRef = React.useRef(void 0); |
| 37 | const reportHeight = React.useCallback( | 37 | const reportHeight = React.useCallback( |
| ... | @@ -40,17 +40,21 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc | ... | @@ -40,17 +40,21 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc |
| 40 | + const height = Math.max(0, rawHeight + heightAdjustment); | 40 | + const height = Math.max(0, rawHeight + heightAdjustment); |
| 41 | var _a; | 41 | var _a; |
| 42 | if (Number.isFinite(height) && height !== lastHeightRef.current) { | 42 | if (Number.isFinite(height) && height !== lastHeightRef.current) { |
| 43 | + const shouldAnimate = lastHeightRef.current !== void 0 && animationDuration > 0; | ||
| 43 | lastHeightRef.current = height; | 44 | lastHeightRef.current = height; |
| 44 | contentInsetEndAdjustment.value = height; | 45 | - contentInsetEndAdjustment.value = height; |
| 45 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); | 46 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); |
| 47 | + contentInsetEndAdjustment.value = shouldAnimate ? reactNativeReanimated.withTiming(height, { | ||
| 48 | + duration: animationDuration | ||
| 49 | + }) : height; | ||
| 46 | } | 50 | } |
| 47 | }, | 51 | }, |
| 48 | - [contentInsetEndAdjustment, listRef] | 52 | - [contentInsetEndAdjustment, listRef] |
| 49 | + [contentInsetEndAdjustment, heightAdjustment, listRef] | 53 | + [animationDuration, contentInsetEndAdjustment, heightAdjustment, listRef] |
| 50 | ); | 54 | ); |
| 51 | React.useLayoutEffect(() => { | 55 | React.useLayoutEffect(() => { |
| 52 | var _a; | 56 | var _a; |
| 53 | @@ -84,9 +84,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { | 57 | @@ -84,9 +87,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { |
| 54 | } | 58 | } |
| 55 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { | 59 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { |
| 56 | const { | 60 | const { |
| ... | @@ -62,7 +66,15 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc | ... | @@ -62,7 +66,15 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc |
| 62 | freeze, | 66 | freeze, |
| 63 | keyboardLiftBehavior, | 67 | keyboardLiftBehavior, |
| 64 | keyboardOffset, | 68 | keyboardOffset, |
| 65 | @@ -108,11 +110,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 69 | @@ -94,6 +99,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 70 | } = props; | ||
| 71 | const refLegendList = React.useRef(null); | ||
| 72 | const combinedRef = useCombinedRef(forwardedRef, refLegendList); | ||
| 73 | + const adjustedStartInsetCompensation = props.contentInsetStartAdjustment; | ||
| 74 | const blankSpace = reactNativeReanimated.useSharedValue(0); | ||
| 75 | React.useEffect(() => { | ||
| 76 | if (!anchoredEndSpace) { | ||
| 77 | @@ -108,11 +114,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 66 | ...anchoredEndSpace, | 78 | ...anchoredEndSpace, |
| 67 | onSizeChanged: (size) => { | 79 | onSizeChanged: (size) => { |
| 68 | var _a; | 80 | var _a; |
| ... | @@ -80,23 +92,25 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc | ... | @@ -80,23 +92,25 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc |
| 80 | const onContentInsetChange = React.useCallback((insets) => { | 92 | const onContentInsetChange = React.useCallback((insets) => { |
| 81 | var _a; | 93 | var _a; |
| 82 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); | 94 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); |
| 83 | @@ -123,6 +129,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 95 | @@ -123,6 +133,8 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 84 | reactNativeKeyboardController.KeyboardChatScrollView, | 96 | reactNativeKeyboardController.KeyboardChatScrollView, |
| 85 | { | 97 | { |
| 86 | ...scrollProps, | 98 | ...scrollProps, |
| 87 | + adjustedInsetCompensation, | 99 | + adjustedInsetCompensation, |
| 100 | + adjustedStartInsetCompensation, | ||
| 88 | applyWorkaroundForContentInsetHitTestBug, | 101 | applyWorkaroundForContentInsetHitTestBug, |
| 89 | blankSpace, | 102 | blankSpace, |
| 90 | extraContentPadding: contentInsetEndAdjustment, | 103 | extraContentPadding: contentInsetEndAdjustment, |
| 91 | @@ -134,6 +141,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 104 | @@ -134,6 +146,8 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 92 | ); | 105 | ); |
| 93 | }, | 106 | }, |
| 94 | [ | 107 | [ |
| 95 | + adjustedInsetCompensation, | 108 | + adjustedInsetCompensation, |
| 109 | + adjustedStartInsetCompensation, | ||
| 96 | applyWorkaroundForContentInsetHitTestBug, | 110 | applyWorkaroundForContentInsetHitTestBug, |
| 97 | blankSpace, | 111 | blankSpace, |
| 98 | contentInsetEndAdjustment, | 112 | contentInsetEndAdjustment, |
| 99 | @@ -149,6 +157,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 113 | @@ -149,6 +163,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 100 | { | 114 | { |
| 101 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, | 115 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, |
| 102 | anchoredEndSpaceOwnerInternal: "scroll", | 116 | anchoredEndSpaceOwnerInternal: "scroll", |
| ... | @@ -105,15 +119,24 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc | ... | @@ -105,15 +119,24 @@ index 6645bcbb1f77c36c432035eaf8b2d6d924fc8bda..74c1f568c485d0f907a0d3ea4ad600cc |
| 105 | renderScrollComponent: memoList, | 119 | renderScrollComponent: memoList, |
| 106 | ...rest | 120 | ...rest |
| 107 | diff --git a/keyboard.mjs b/keyboard.mjs | 121 | diff --git a/keyboard.mjs b/keyboard.mjs |
| 108 | index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..62206d657d5616893985fd2c228e57af7eba744b 100644 | 122 | index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..111eb242ba9d9ade1cb912550f7df85cf4f361ee 100644 |
| 109 | --- a/keyboard.mjs | 123 | --- a/keyboard.mjs |
| 110 | +++ b/keyboard.mjs | 124 | +++ b/keyboard.mjs |
| 111 | @@ -12,19 +12,19 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !KeyboardChatScrollView) { | 125 | @@ -1,7 +1,7 @@ |
| 126 | import * as React from 'react'; | ||
| 127 | import { useRef, useEffect, useMemo, useCallback, useLayoutEffect } from 'react'; | ||
| 128 | import { KeyboardChatScrollView, KeyboardController } from 'react-native-keyboard-controller'; | ||
| 129 | -import { useSharedValue } from 'react-native-reanimated'; | ||
| 130 | +import { useSharedValue, withTiming } from 'react-native-reanimated'; | ||
| 131 | import { internal } from '@legendapp/list/react-native'; | ||
| 132 | import { AnimatedLegendList } from '@legendapp/list/reanimated'; | ||
| 133 | |||
| 134 | @@ -12,19 +12,22 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !KeyboardChatScrollView) { | ||
| 112 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." | 135 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." |
| 113 | ); | 136 | ); |
| 114 | } | 137 | } |
| 115 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { | 138 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { |
| 116 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0) { | 139 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0, animationDuration = 0) { |
| 117 | const contentInsetEndAdjustment = useSharedValue(initialHeight); | 140 | const contentInsetEndAdjustment = useSharedValue(initialHeight); |
| 118 | const lastHeightRef = useRef(void 0); | 141 | const lastHeightRef = useRef(void 0); |
| 119 | const reportHeight = useCallback( | 142 | const reportHeight = useCallback( |
| ... | @@ -122,17 +145,21 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..62206d657d5616893985fd2c228e57af | ... | @@ -122,17 +145,21 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..62206d657d5616893985fd2c228e57af |
| 122 | + const height = Math.max(0, rawHeight + heightAdjustment); | 145 | + const height = Math.max(0, rawHeight + heightAdjustment); |
| 123 | var _a; | 146 | var _a; |
| 124 | if (Number.isFinite(height) && height !== lastHeightRef.current) { | 147 | if (Number.isFinite(height) && height !== lastHeightRef.current) { |
| 148 | + const shouldAnimate = lastHeightRef.current !== void 0 && animationDuration > 0; | ||
| 125 | lastHeightRef.current = height; | 149 | lastHeightRef.current = height; |
| 126 | contentInsetEndAdjustment.value = height; | 150 | - contentInsetEndAdjustment.value = height; |
| 127 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); | 151 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); |
| 152 | + contentInsetEndAdjustment.value = shouldAnimate ? withTiming(height, { | ||
| 153 | + duration: animationDuration | ||
| 154 | + }) : height; | ||
| 128 | } | 155 | } |
| 129 | }, | 156 | }, |
| 130 | - [contentInsetEndAdjustment, listRef] | 157 | - [contentInsetEndAdjustment, listRef] |
| 131 | + [contentInsetEndAdjustment, heightAdjustment, listRef] | 158 | + [animationDuration, contentInsetEndAdjustment, heightAdjustment, listRef] |
| 132 | ); | 159 | ); |
| 133 | useLayoutEffect(() => { | 160 | useLayoutEffect(() => { |
| 134 | var _a; | 161 | var _a; |
| 135 | @@ -63,9 +63,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { | 162 | @@ -63,9 +66,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { |
| 136 | } | 163 | } |
| 137 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { | 164 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { |
| 138 | const { | 165 | const { |
| ... | @@ -144,7 +171,15 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..62206d657d5616893985fd2c228e57af | ... | @@ -144,7 +171,15 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..62206d657d5616893985fd2c228e57af |
| 144 | freeze, | 171 | freeze, |
| 145 | keyboardLiftBehavior, | 172 | keyboardLiftBehavior, |
| 146 | keyboardOffset, | 173 | keyboardOffset, |
| 147 | @@ -87,11 +89,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 174 | @@ -73,6 +78,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 175 | } = props; | ||
| 176 | const refLegendList = useRef(null); | ||
| 177 | const combinedRef = useCombinedRef(forwardedRef, refLegendList); | ||
| 178 | + const adjustedStartInsetCompensation = props.contentInsetStartAdjustment; | ||
| 179 | const blankSpace = useSharedValue(0); | ||
| 180 | useEffect(() => { | ||
| 181 | if (!anchoredEndSpace) { | ||
| 182 | @@ -87,11 +93,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 148 | ...anchoredEndSpace, | 183 | ...anchoredEndSpace, |
| 149 | onSizeChanged: (size) => { | 184 | onSizeChanged: (size) => { |
| 150 | var _a; | 185 | var _a; |
| ... | @@ -162,23 +197,25 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..62206d657d5616893985fd2c228e57af | ... | @@ -162,23 +197,25 @@ index 87b38b9607c2eba6b407c2acfd520633bdb0e7c2..62206d657d5616893985fd2c228e57af |
| 162 | const onContentInsetChange = useCallback((insets) => { | 197 | const onContentInsetChange = useCallback((insets) => { |
| 163 | var _a; | 198 | var _a; |
| 164 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); | 199 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); |
| 165 | @@ -102,6 +108,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 200 | @@ -102,6 +112,8 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 166 | KeyboardChatScrollView, | 201 | KeyboardChatScrollView, |
| 167 | { | 202 | { |
| 168 | ...scrollProps, | 203 | ...scrollProps, |
| 169 | + adjustedInsetCompensation, | 204 | + adjustedInsetCompensation, |
| 205 | + adjustedStartInsetCompensation, | ||
| 170 | applyWorkaroundForContentInsetHitTestBug, | 206 | applyWorkaroundForContentInsetHitTestBug, |
| 171 | blankSpace, | 207 | blankSpace, |
| 172 | extraContentPadding: contentInsetEndAdjustment, | 208 | extraContentPadding: contentInsetEndAdjustment, |
| 173 | @@ -113,6 +120,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 209 | @@ -113,6 +125,8 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 174 | ); | 210 | ); |
| 175 | }, | 211 | }, |
| 176 | [ | 212 | [ |
| 177 | + adjustedInsetCompensation, | 213 | + adjustedInsetCompensation, |
| 214 | + adjustedStartInsetCompensation, | ||
| 178 | applyWorkaroundForContentInsetHitTestBug, | 215 | applyWorkaroundForContentInsetHitTestBug, |
| 179 | blankSpace, | 216 | blankSpace, |
| 180 | contentInsetEndAdjustment, | 217 | contentInsetEndAdjustment, |
| 181 | @@ -128,6 +136,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | 218 | @@ -128,6 +142,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( |
| 182 | { | 219 | { |
| 183 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, | 220 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, |
| 184 | anchoredEndSpaceOwnerInternal: "scroll", | 221 | anchoredEndSpaceOwnerInternal: "scroll", |
| ... | @@ -204,7 +241,7 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a | ... | @@ -204,7 +241,7 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a |
| 204 | * Number of columns to render items in. | 241 | * Number of columns to render items in. |
| 205 | * @default 1 | 242 | * @default 1 |
| 206 | diff --git a/react-native.js b/react-native.js | 243 | diff --git a/react-native.js b/react-native.js |
| 207 | index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4a5493efb 100644 | 244 | index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c3ffe83af 100644 |
| 208 | --- a/react-native.js | 245 | --- a/react-native.js |
| 209 | +++ b/react-native.js | 246 | +++ b/react-native.js |
| 210 | @@ -954,7 +954,7 @@ function setInitialRenderState(ctx, { | 247 | @@ -954,7 +954,7 @@ function setInitialRenderState(ctx, { |
| ... | @@ -216,7 +253,27 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -216,7 +253,27 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 216 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { | 253 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { |
| 217 | set$(ctx, "readyToRender", true); | 254 | set$(ctx, "readyToRender", true); |
| 218 | setAdaptiveRender(ctx, "normal", "ready"); | 255 | setAdaptiveRender(ctx, "normal", "ready"); |
| 219 | @@ -1304,18 +1304,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | 256 | @@ -1090,7 +1090,7 @@ function getRawContentLength(ctx) { |
| 257 | function getAlignItemsAtEndPadding(ctx) { | ||
| 258 | const { state } = ctx; | ||
| 259 | const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; | ||
| 260 | - return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; | ||
| 261 | + return shouldPad ? Math.max(0, state.scrollLength - getContentInsetStartAdjustment(ctx) - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; | ||
| 262 | } | ||
| 263 | function updateContentMetricsState(ctx) { | ||
| 264 | const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; | ||
| 265 | @@ -1115,6 +1115,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) { | ||
| 266 | totalSize += add; | ||
| 267 | } | ||
| 268 | if (prevTotalSize !== totalSize) { | ||
| 269 | + const now = Date.now(); | ||
| 270 | + const isContinuingContentSizeAnimation = state.contentSizeAnimationActiveEpoch !== void 0; | ||
| 271 | + state.contentSizeAnimationEpoch = (state.contentSizeAnimationEpoch || 0) + 1; | ||
| 272 | + state.contentSizeAnimationEligible = !!state.props.sizeComponentInternal && state.didContainersLayout && (isContinuingContentSizeAnimation || !state.props.maintainScrollAtEnd && !state.isUserDragging && !state.isMomentumScrolling && now - (state.lastMVCPAdjustTime || 0) >= 300); | ||
| 273 | if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) { | ||
| 274 | state.pendingTotalSize = totalSize; | ||
| 275 | } else { | ||
| 276 | @@ -1304,18 +1308,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | ||
| 220 | } | 277 | } |
| 221 | 278 | ||
| 222 | // src/core/clampScrollOffset.ts | 279 | // src/core/clampScrollOffset.ts |
| ... | @@ -242,7 +299,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -242,7 +299,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 242 | return clampedOffset; | 299 | return clampedOffset; |
| 243 | } | 300 | } |
| 244 | 301 | ||
| 245 | @@ -1451,10 +1456,10 @@ function checkFinishedScrollFrame(ctx) { | 302 | @@ -1451,10 +1460,10 @@ function checkFinishedScrollFrame(ctx) { |
| 246 | finishScrollTo(ctx); | 303 | finishScrollTo(ctx); |
| 247 | } | 304 | } |
| 248 | } | 305 | } |
| ... | @@ -255,7 +312,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -255,7 +312,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 255 | x: ctx.state.props.horizontal ? offset : 0, | 312 | x: ctx.state.props.horizontal ? offset : 0, |
| 256 | y: ctx.state.props.horizontal ? 0 : offset | 313 | y: ctx.state.props.horizontal ? 0 : offset |
| 257 | }); | 314 | }); |
| 258 | @@ -1503,7 +1508,10 @@ function checkFinishedScrollFallback(ctx) { | 315 | @@ -1503,7 +1512,10 @@ function checkFinishedScrollFallback(ctx) { |
| 259 | ); | 316 | ); |
| 260 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); | 317 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); |
| 261 | } else if (shouldRetryUnalignedEndScroll) { | 318 | } else if (shouldRetryUnalignedEndScroll) { |
| ... | @@ -267,7 +324,17 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -267,7 +324,17 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 267 | scheduleFallbackCheck(100); | 324 | scheduleFallbackCheck(100); |
| 268 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { | 325 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { |
| 269 | finishScrollTo(ctx); | 326 | finishScrollTo(ctx); |
| 270 | @@ -1566,9 +1574,18 @@ function doMaintainScrollAtEnd(ctx) { | 327 | @@ -1560,15 +1572,28 @@ function doMaintainScrollAtEnd(ctx) { |
| 328 | } = state; | ||
| 329 | const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); | ||
| 330 | const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); | ||
| 331 | + if (state.contentSizeAnimationActiveEpoch !== void 0) { | ||
| 332 | + state.pendingMaintainScrollAtEnd = false; | ||
| 333 | + return false; | ||
| 334 | + } | ||
| 335 | if (pendingNativeMVCPAdjust) { | ||
| 336 | state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; | ||
| 337 | return false; | ||
| 271 | } | 338 | } |
| 272 | if (shouldMaintainScrollAtEnd) { | 339 | if (shouldMaintainScrollAtEnd) { |
| 273 | state.pendingMaintainScrollAtEnd = false; | 340 | state.pendingMaintainScrollAtEnd = false; |
| ... | @@ -287,7 +354,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -287,7 +354,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 287 | } | 354 | } |
| 288 | if (!state.maintainingScrollAtEnd) { | 355 | if (!state.maintainingScrollAtEnd) { |
| 289 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; | 356 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; |
| 290 | @@ -1591,9 +1608,18 @@ function doMaintainScrollAtEnd(ctx) { | 357 | @@ -1591,9 +1616,18 @@ function doMaintainScrollAtEnd(ctx) { |
| 291 | y: 0 | 358 | y: 0 |
| 292 | }); | 359 | }); |
| 293 | } else { | 360 | } else { |
| ... | @@ -309,7 +376,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -309,7 +376,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 309 | } | 376 | } |
| 310 | setTimeout( | 377 | setTimeout( |
| 311 | () => { | 378 | () => { |
| 312 | @@ -1624,6 +1650,10 @@ function doMaintainScrollAtEnd(ctx) { | 379 | @@ -1624,6 +1658,10 @@ function doMaintainScrollAtEnd(ctx) { |
| 313 | function requestAdjust(ctx, positionDiff, dataChanged) { | 380 | function requestAdjust(ctx, positionDiff, dataChanged) { |
| 314 | const state = ctx.state; | 381 | const state = ctx.state; |
| 315 | if (Math.abs(positionDiff) > 0.1) { | 382 | if (Math.abs(positionDiff) > 0.1) { |
| ... | @@ -320,7 +387,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -320,7 +387,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 320 | const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; | 387 | const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; |
| 321 | const doit = () => { | 388 | const doit = () => { |
| 322 | if (needsScrollWorkaround) { | 389 | if (needsScrollWorkaround) { |
| 323 | @@ -1728,7 +1758,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { | 390 | @@ -1728,7 +1766,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { |
| 324 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { | 391 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { |
| 325 | return 0; | 392 | return 0; |
| 326 | } | 393 | } |
| ... | @@ -331,7 +398,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -331,7 +398,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 331 | const clampDelta = maxScroll - state.scroll; | 398 | const clampDelta = maxScroll - state.scroll; |
| 332 | if (unresolvedAmount < 0) { | 399 | if (unresolvedAmount < 0) { |
| 333 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); | 400 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); |
| 334 | @@ -1790,7 +1822,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { | 401 | @@ -1790,7 +1830,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { |
| 335 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); | 402 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); |
| 336 | return true; | 403 | return true; |
| 337 | } | 404 | } |
| ... | @@ -340,7 +407,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -340,7 +407,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 340 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); | 407 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); |
| 341 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; | 408 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; |
| 342 | if (isAtExpectedNativeClamp) { | 409 | if (isAtExpectedNativeClamp) { |
| 343 | @@ -1923,7 +1955,7 @@ function prepareMVCP(ctx, dataChanged) { | 410 | @@ -1923,7 +1963,7 @@ function prepareMVCP(ctx, dataChanged) { |
| 344 | if (diff > 0) { | 411 | if (diff > 0) { |
| 345 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); | 412 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); |
| 346 | } else { | 413 | } else { |
| ... | @@ -349,7 +416,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -349,7 +416,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 349 | state.scroll = maxScroll; | 416 | state.scroll = maxScroll; |
| 350 | state.scrollPending = maxScroll; | 417 | state.scrollPending = maxScroll; |
| 351 | diff = 0; | 418 | diff = 0; |
| 352 | @@ -2320,8 +2352,121 @@ function scrollToIndex(ctx, { | 419 | @@ -2320,8 +2360,121 @@ function scrollToIndex(ctx, { |
| 353 | } | 420 | } |
| 354 | 421 | ||
| 355 | // src/core/initialScroll.ts | 422 | // src/core/initialScroll.ts |
| ... | @@ -471,7 +538,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -471,7 +538,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 471 | const requestedIndex = target.index; | 538 | const requestedIndex = target.index; |
| 472 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; | 539 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; |
| 473 | const itemSize = getItemSizeAtIndex(ctx, index); | 540 | const itemSize = getItemSizeAtIndex(ctx, index); |
| 474 | @@ -2747,7 +2892,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { | 541 | @@ -2747,7 +2900,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { |
| 475 | return; | 542 | return; |
| 476 | } | 543 | } |
| 477 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { | 544 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { |
| ... | @@ -482,7 +549,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -482,7 +549,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 482 | if (!shouldKeepEndTargetAlive) { | 549 | if (!shouldKeepEndTargetAlive) { |
| 483 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { | 550 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { |
| 484 | clearPendingInitialScrollFooterLayout(ctx, { | 551 | clearPendingInitialScrollFooterLayout(ctx, { |
| 485 | @@ -4672,7 +4819,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 552 | @@ -4672,7 +4827,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| 486 | contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap); | 553 | contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap); |
| 487 | contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props); | 554 | contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props); |
| 488 | isReady = !hasUnknownTailSize; | 555 | isReady = !hasUnknownTailSize; |
| ... | @@ -492,7 +559,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -492,7 +559,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 492 | } else if (anchorIndex >= 0) { | 559 | } else if (anchorIndex >= 0) { |
| 493 | isReady = false; | 560 | isReady = false; |
| 494 | } | 561 | } |
| 495 | @@ -4692,6 +4840,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 562 | @@ -4692,6 +4848,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| 496 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); | 563 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); |
| 497 | } | 564 | } |
| 498 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | 565 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); |
| ... | @@ -505,7 +572,84 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -505,7 +572,84 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 505 | } | 572 | } |
| 506 | return nextSize; | 573 | return nextSize; |
| 507 | } | 574 | } |
| 508 | @@ -7075,6 +7229,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 575 | @@ -5715,6 +5877,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ |
| 576 | horizontal | ||
| 577 | }) { | ||
| 578 | const ctx = useStateContext(); | ||
| 579 | + const SizeComponent = ctx.state.props.sizeComponentInternal; | ||
| 580 | const columnWrapperStyle = ctx.columnWrapperStyle; | ||
| 581 | const animSize = useValue$("totalSize"); | ||
| 582 | const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]); | ||
| 583 | @@ -5725,6 +5888,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ | ||
| 584 | opacity: isVisible ? 1 : 0, | ||
| 585 | width: animSize | ||
| 586 | } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 }; | ||
| 587 | + if (SizeComponent) { | ||
| 588 | + if (horizontal) { | ||
| 589 | + delete style.width; | ||
| 590 | + } else { | ||
| 591 | + delete style.height; | ||
| 592 | + } | ||
| 593 | + } | ||
| 594 | if (columnWrapperStyle) { | ||
| 595 | const { columnGap, rowGap, gap } = columnWrapperStyle; | ||
| 596 | const gapX = columnGap || gap || 0; | ||
| 597 | @@ -5745,7 +5915,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ | ||
| 598 | } | ||
| 599 | } | ||
| 600 | } | ||
| 601 | - return /* @__PURE__ */ React2__namespace.createElement(ReactNative.Animated.View, { pointerEvents: isVisible ? void 0 : "none", style }, /* @__PURE__ */ React2__namespace.createElement(ContainerLayoutCoordinator, null, children)); | ||
| 602 | + const content = /* @__PURE__ */ React2__namespace.createElement(ContainerLayoutCoordinator, null, children); | ||
| 603 | + return SizeComponent ? /* @__PURE__ */ React2__namespace.createElement(SizeComponent, { horizontal, pointerEvents: isVisible ? void 0 : "none", signalName: "totalSize", style }, content) : /* @__PURE__ */ React2__namespace.createElement(ReactNative.Animated.View, { pointerEvents: isVisible ? void 0 : "none", style }, content); | ||
| 604 | }); | ||
| 605 | var Containers = typedMemo(function Containers2({ | ||
| 606 | freshDataTransitionEpoch, | ||
| 607 | @@ -5896,7 +6067,12 @@ var StyleSheet = ReactNative.StyleSheet; | ||
| 608 | |||
| 609 | // src/components/ListComponent.tsx | ||
| 610 | var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) { | ||
| 611 | + const ctx = useStateContext(); | ||
| 612 | const [alignItemsAtEndPadding = 0] = useArr$(["alignItemsAtEndPadding"]); | ||
| 613 | + const SizeComponent = ctx.state.props.sizeComponentInternal; | ||
| 614 | + if (SizeComponent) { | ||
| 615 | + return /* @__PURE__ */ React2__namespace.createElement(SizeComponent, { horizontal, signalName: "alignItemsAtEndPadding", style: { flexShrink: 0 } }); | ||
| 616 | + } | ||
| 617 | if (alignItemsAtEndPadding <= 0) { | ||
| 618 | return null; | ||
| 619 | } | ||
| 620 | @@ -5929,8 +6105,12 @@ var ListComponent = typedMemo(function ListComponent2({ | ||
| 621 | refScrollView, | ||
| 622 | renderScrollComponent, | ||
| 623 | onLayoutFooter, | ||
| 624 | + onInternalMomentumScrollBegin, | ||
| 625 | onInternalScrollBeginDrag, | ||
| 626 | + onInternalScrollEndDrag, | ||
| 627 | onInternalScrollEnd, | ||
| 628 | + onMomentumScrollBegin, | ||
| 629 | + onScrollEndDrag, | ||
| 630 | scrollAdjustHandler, | ||
| 631 | snapToIndices, | ||
| 632 | stickyHeaderConfig, | ||
| 633 | @@ -6001,7 +6181,17 @@ var ListComponent = typedMemo(function ListComponent2({ | ||
| 634 | SnapOrScroll, | ||
| 635 | { | ||
| 636 | ...rest, | ||
| 637 | - ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, | ||
| 638 | + ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { | ||
| 639 | + onMomentumScrollBegin: (event) => { | ||
| 640 | + onInternalMomentumScrollBegin == null ? void 0 : onInternalMomentumScrollBegin(event); | ||
| 641 | + onMomentumScrollBegin == null ? void 0 : onMomentumScrollBegin(event); | ||
| 642 | + }, | ||
| 643 | + onScrollBeginDrag: onInternalScrollBeginDrag, | ||
| 644 | + onScrollEndDrag: (event) => { | ||
| 645 | + onInternalScrollEndDrag == null ? void 0 : onInternalScrollEndDrag(event); | ||
| 646 | + onScrollEndDrag == null ? void 0 : onScrollEndDrag(event); | ||
| 647 | + } | ||
| 648 | + }, | ||
| 649 | contentContainerStyle: [ | ||
| 650 | horizontal ? { height: "100%" } : {}, | ||
| 651 | contentContainerStyle, | ||
| 652 | @@ -7075,6 +7265,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 509 | dataVersion, | 653 | dataVersion, |
| 510 | drawDistance = 250, | 654 | drawDistance = 250, |
| 511 | contentInsetEndAdjustment, | 655 | contentInsetEndAdjustment, |
| ... | @@ -513,7 +657,20 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -513,7 +657,20 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 513 | estimatedItemSize = 100, | 657 | estimatedItemSize = 100, |
| 514 | estimatedListSize, | 658 | estimatedListSize, |
| 515 | extraData, | 659 | extraData, |
| 516 | @@ -7200,7 +7356,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 660 | @@ -7132,10 +7323,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 661 | const animatedPropsInternal = props.animatedPropsInternal; | ||
| 662 | const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list"; | ||
| 663 | const positionComponentInternal = props.positionComponentInternal; | ||
| 664 | + const sizeComponentInternal = props.sizeComponentInternal; | ||
| 665 | const stickyPositionComponentInternal = props.stickyPositionComponentInternal; | ||
| 666 | const { | ||
| 667 | anchoredEndSpaceOwnerInternal: _anchoredEndSpaceOwnerInternal, | ||
| 668 | positionComponentInternal: _positionComponentInternal, | ||
| 669 | + sizeComponentInternal: _sizeComponentInternal, | ||
| 670 | stickyPositionComponentInternal: _stickyPositionComponentInternal, | ||
| 671 | ...restProps | ||
| 672 | } = rest; | ||
| 673 | @@ -7200,7 +7393,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 517 | const combinedRef = useCombinedRef(refScroller, refScrollView); | 674 | const combinedRef = useCombinedRef(refScroller, refScrollView); |
| 518 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); | 675 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); |
| 519 | const stickyHeaderIndices = stickyHeaderIndicesProp; | 676 | const stickyHeaderIndices = stickyHeaderIndicesProp; |
| ... | @@ -522,7 +679,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -522,7 +679,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 522 | const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); | 679 | const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); |
| 523 | const alwaysRenderIndices = React2.useMemo(() => { | 680 | const alwaysRenderIndices = React2.useMemo(() => { |
| 524 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); | 681 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); |
| 525 | @@ -7341,6 +7497,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 682 | @@ -7341,6 +7534,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 526 | contentContainerAlignItems: contentContainerStyle.alignItems, | 683 | contentContainerAlignItems: contentContainerStyle.alignItems, |
| 527 | contentInset, | 684 | contentInset, |
| 528 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, | 685 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, |
| ... | @@ -530,7 +687,15 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -530,7 +687,15 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 530 | data: dataProp, | 687 | data: dataProp, |
| 531 | dataKey, | 688 | dataKey, |
| 532 | dataVersion, | 689 | dataVersion, |
| 533 | @@ -7423,6 +7580,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 690 | @@ -7372,6 +7566,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 691 | renderItem, | ||
| 692 | rtl, | ||
| 693 | snapToIndices, | ||
| 694 | + sizeComponentInternal, | ||
| 695 | stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [], | ||
| 696 | stickyHeaderIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]), | ||
| 697 | stickyPositionComponentInternal, | ||
| 698 | @@ -7423,6 +7618,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 534 | return void 0; | 699 | return void 0; |
| 535 | } | 700 | } |
| 536 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); | 701 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); |
| ... | @@ -544,15 +709,43 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -544,15 +709,43 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 544 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; | 709 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; |
| 545 | }, [usesBootstrapInitialScroll]); | 710 | }, [usesBootstrapInitialScroll]); |
| 546 | React2.useLayoutEffect(() => { | 711 | React2.useLayoutEffect(() => { |
| 547 | @@ -7651,6 +7815,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 712 | @@ -7547,6 +7749,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 713 | [ | ||
| 714 | dataKey, | ||
| 715 | dataVersion, | ||
| 716 | + contentInsetStartAdjustment, | ||
| 717 | memoizedLastItemKeys.join(","), | ||
| 718 | numColumnsProp, | ||
| 719 | nextScrollAxisGap, | ||
| 720 | @@ -7643,6 +7846,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 721 | () => ({ | ||
| 722 | getRenderedItem: (key) => getRenderedItem(ctx, key), | ||
| 723 | onMomentumScrollEnd: (event) => { | ||
| 724 | + state.isMomentumScrolling = false; | ||
| 725 | checkFinishedScrollFallback(ctx); | ||
| 726 | if (state.props.onMomentumScrollEnd) { | ||
| 727 | state.props.onMomentumScrollEnd(event); | ||
| 728 | @@ -7651,6 +7855,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 548 | onScroll: (event) => onScroll(ctx, event), | 729 | onScroll: (event) => onScroll(ctx, event), |
| 549 | onScrollBeginDrag: (event) => { | 730 | onScrollBeginDrag: (event) => { |
| 550 | var _a4, _b2; | 731 | var _a4, _b2; |
| 551 | + ctx.state.didUserDrag = true; | 732 | + ctx.state.didUserDrag = true; |
| 733 | + ctx.state.isUserDragging = true; | ||
| 552 | prepareReachedEdgeForNextUserScroll(ctx); | 734 | prepareReachedEdgeForNextUserScroll(ctx); |
| 553 | (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); | 735 | (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); |
| 554 | }, | 736 | }, |
| 555 | @@ -7681,6 +7846,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 737 | @@ -7676,11 +7882,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 738 | ListFooterComponent, | ||
| 739 | ListFooterComponentStyle, | ||
| 740 | ListHeaderComponent, | ||
| 741 | + onInternalMomentumScrollBegin: () => { | ||
| 742 | + ctx.state.isMomentumScrolling = true; | ||
| 743 | + }, | ||
| 744 | onInternalScrollBeginDrag: fns.onScrollBeginDrag, | ||
| 745 | + onInternalScrollEndDrag: () => { | ||
| 746 | + ctx.state.isUserDragging = false; | ||
| 747 | + }, | ||
| 748 | onInternalScrollEnd: fns.onScrollEnd, | ||
| 556 | onLayout, | 749 | onLayout, |
| 557 | onLayoutFooter, | 750 | onLayoutFooter, |
| 558 | onMomentumScrollEnd: fns.onMomentumScrollEnd, | 751 | onMomentumScrollEnd: fns.onMomentumScrollEnd, |
| ... | @@ -561,7 +754,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 | ... | @@ -561,7 +754,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..4227ed22da8865b7493f47f199f4d8f4 |
| 561 | recycleItems, | 754 | recycleItems, |
| 562 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { | 755 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { |
| 563 | diff --git a/react-native.mjs b/react-native.mjs | 756 | diff --git a/react-native.mjs b/react-native.mjs |
| 564 | index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c7a5dce7f 100644 | 757 | index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318d57e29dc 100644 |
| 565 | --- a/react-native.mjs | 758 | --- a/react-native.mjs |
| 566 | +++ b/react-native.mjs | 759 | +++ b/react-native.mjs |
| 567 | @@ -933,7 +933,7 @@ function setInitialRenderState(ctx, { | 760 | @@ -933,7 +933,7 @@ function setInitialRenderState(ctx, { |
| ... | @@ -573,7 +766,27 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -573,7 +766,27 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 573 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { | 766 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { |
| 574 | set$(ctx, "readyToRender", true); | 767 | set$(ctx, "readyToRender", true); |
| 575 | setAdaptiveRender(ctx, "normal", "ready"); | 768 | setAdaptiveRender(ctx, "normal", "ready"); |
| 576 | @@ -1283,18 +1283,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | 769 | @@ -1069,7 +1069,7 @@ function getRawContentLength(ctx) { |
| 770 | function getAlignItemsAtEndPadding(ctx) { | ||
| 771 | const { state } = ctx; | ||
| 772 | const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; | ||
| 773 | - return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; | ||
| 774 | + return shouldPad ? Math.max(0, state.scrollLength - getContentInsetStartAdjustment(ctx) - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; | ||
| 775 | } | ||
| 776 | function updateContentMetricsState(ctx) { | ||
| 777 | const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; | ||
| 778 | @@ -1094,6 +1094,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) { | ||
| 779 | totalSize += add; | ||
| 780 | } | ||
| 781 | if (prevTotalSize !== totalSize) { | ||
| 782 | + const now = Date.now(); | ||
| 783 | + const isContinuingContentSizeAnimation = state.contentSizeAnimationActiveEpoch !== void 0; | ||
| 784 | + state.contentSizeAnimationEpoch = (state.contentSizeAnimationEpoch || 0) + 1; | ||
| 785 | + state.contentSizeAnimationEligible = !!state.props.sizeComponentInternal && state.didContainersLayout && (isContinuingContentSizeAnimation || !state.props.maintainScrollAtEnd && !state.isUserDragging && !state.isMomentumScrolling && now - (state.lastMVCPAdjustTime || 0) >= 300); | ||
| 786 | if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) { | ||
| 787 | state.pendingTotalSize = totalSize; | ||
| 788 | } else { | ||
| 789 | @@ -1283,18 +1287,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | ||
| 577 | } | 790 | } |
| 578 | 791 | ||
| 579 | // src/core/clampScrollOffset.ts | 792 | // src/core/clampScrollOffset.ts |
| ... | @@ -599,7 +812,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -599,7 +812,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 599 | return clampedOffset; | 812 | return clampedOffset; |
| 600 | } | 813 | } |
| 601 | 814 | ||
| 602 | @@ -1430,10 +1435,10 @@ function checkFinishedScrollFrame(ctx) { | 815 | @@ -1430,10 +1439,10 @@ function checkFinishedScrollFrame(ctx) { |
| 603 | finishScrollTo(ctx); | 816 | finishScrollTo(ctx); |
| 604 | } | 817 | } |
| 605 | } | 818 | } |
| ... | @@ -612,7 +825,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -612,7 +825,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 612 | x: ctx.state.props.horizontal ? offset : 0, | 825 | x: ctx.state.props.horizontal ? offset : 0, |
| 613 | y: ctx.state.props.horizontal ? 0 : offset | 826 | y: ctx.state.props.horizontal ? 0 : offset |
| 614 | }); | 827 | }); |
| 615 | @@ -1482,7 +1487,10 @@ function checkFinishedScrollFallback(ctx) { | 828 | @@ -1482,7 +1491,10 @@ function checkFinishedScrollFallback(ctx) { |
| 616 | ); | 829 | ); |
| 617 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); | 830 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); |
| 618 | } else if (shouldRetryUnalignedEndScroll) { | 831 | } else if (shouldRetryUnalignedEndScroll) { |
| ... | @@ -624,7 +837,17 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -624,7 +837,17 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 624 | scheduleFallbackCheck(100); | 837 | scheduleFallbackCheck(100); |
| 625 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { | 838 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { |
| 626 | finishScrollTo(ctx); | 839 | finishScrollTo(ctx); |
| 627 | @@ -1545,9 +1553,18 @@ function doMaintainScrollAtEnd(ctx) { | 840 | @@ -1539,15 +1551,28 @@ function doMaintainScrollAtEnd(ctx) { |
| 841 | } = state; | ||
| 842 | const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); | ||
| 843 | const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); | ||
| 844 | + if (state.contentSizeAnimationActiveEpoch !== void 0) { | ||
| 845 | + state.pendingMaintainScrollAtEnd = false; | ||
| 846 | + return false; | ||
| 847 | + } | ||
| 848 | if (pendingNativeMVCPAdjust) { | ||
| 849 | state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; | ||
| 850 | return false; | ||
| 628 | } | 851 | } |
| 629 | if (shouldMaintainScrollAtEnd) { | 852 | if (shouldMaintainScrollAtEnd) { |
| 630 | state.pendingMaintainScrollAtEnd = false; | 853 | state.pendingMaintainScrollAtEnd = false; |
| ... | @@ -644,7 +867,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -644,7 +867,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 644 | } | 867 | } |
| 645 | if (!state.maintainingScrollAtEnd) { | 868 | if (!state.maintainingScrollAtEnd) { |
| 646 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; | 869 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; |
| 647 | @@ -1570,9 +1587,18 @@ function doMaintainScrollAtEnd(ctx) { | 870 | @@ -1570,9 +1595,18 @@ function doMaintainScrollAtEnd(ctx) { |
| 648 | y: 0 | 871 | y: 0 |
| 649 | }); | 872 | }); |
| 650 | } else { | 873 | } else { |
| ... | @@ -666,7 +889,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -666,7 +889,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 666 | } | 889 | } |
| 667 | setTimeout( | 890 | setTimeout( |
| 668 | () => { | 891 | () => { |
| 669 | @@ -1603,6 +1629,10 @@ function doMaintainScrollAtEnd(ctx) { | 892 | @@ -1603,6 +1637,10 @@ function doMaintainScrollAtEnd(ctx) { |
| 670 | function requestAdjust(ctx, positionDiff, dataChanged) { | 893 | function requestAdjust(ctx, positionDiff, dataChanged) { |
| 671 | const state = ctx.state; | 894 | const state = ctx.state; |
| 672 | if (Math.abs(positionDiff) > 0.1) { | 895 | if (Math.abs(positionDiff) > 0.1) { |
| ... | @@ -677,7 +900,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -677,7 +900,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 677 | const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; | 900 | const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; |
| 678 | const doit = () => { | 901 | const doit = () => { |
| 679 | if (needsScrollWorkaround) { | 902 | if (needsScrollWorkaround) { |
| 680 | @@ -1707,7 +1737,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { | 903 | @@ -1707,7 +1745,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { |
| 681 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { | 904 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { |
| 682 | return 0; | 905 | return 0; |
| 683 | } | 906 | } |
| ... | @@ -688,7 +911,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -688,7 +911,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 688 | const clampDelta = maxScroll - state.scroll; | 911 | const clampDelta = maxScroll - state.scroll; |
| 689 | if (unresolvedAmount < 0) { | 912 | if (unresolvedAmount < 0) { |
| 690 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); | 913 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); |
| 691 | @@ -1769,7 +1801,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { | 914 | @@ -1769,7 +1809,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { |
| 692 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); | 915 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); |
| 693 | return true; | 916 | return true; |
| 694 | } | 917 | } |
| ... | @@ -697,7 +920,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -697,7 +920,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 697 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); | 920 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); |
| 698 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; | 921 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; |
| 699 | if (isAtExpectedNativeClamp) { | 922 | if (isAtExpectedNativeClamp) { |
| 700 | @@ -1902,7 +1934,7 @@ function prepareMVCP(ctx, dataChanged) { | 923 | @@ -1902,7 +1942,7 @@ function prepareMVCP(ctx, dataChanged) { |
| 701 | if (diff > 0) { | 924 | if (diff > 0) { |
| 702 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); | 925 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); |
| 703 | } else { | 926 | } else { |
| ... | @@ -706,7 +929,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -706,7 +929,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 706 | state.scroll = maxScroll; | 929 | state.scroll = maxScroll; |
| 707 | state.scrollPending = maxScroll; | 930 | state.scrollPending = maxScroll; |
| 708 | diff = 0; | 931 | diff = 0; |
| 709 | @@ -2299,8 +2331,121 @@ function scrollToIndex(ctx, { | 932 | @@ -2299,8 +2339,121 @@ function scrollToIndex(ctx, { |
| 710 | } | 933 | } |
| 711 | 934 | ||
| 712 | // src/core/initialScroll.ts | 935 | // src/core/initialScroll.ts |
| ... | @@ -828,7 +1051,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -828,7 +1051,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 828 | const requestedIndex = target.index; | 1051 | const requestedIndex = target.index; |
| 829 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; | 1052 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; |
| 830 | const itemSize = getItemSizeAtIndex(ctx, index); | 1053 | const itemSize = getItemSizeAtIndex(ctx, index); |
| 831 | @@ -2726,7 +2871,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { | 1054 | @@ -2726,7 +2879,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { |
| 832 | return; | 1055 | return; |
| 833 | } | 1056 | } |
| 834 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { | 1057 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { |
| ... | @@ -839,7 +1062,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -839,7 +1062,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 839 | if (!shouldKeepEndTargetAlive) { | 1062 | if (!shouldKeepEndTargetAlive) { |
| 840 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { | 1063 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { |
| 841 | clearPendingInitialScrollFooterLayout(ctx, { | 1064 | clearPendingInitialScrollFooterLayout(ctx, { |
| 842 | @@ -4651,7 +4798,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1065 | @@ -4651,7 +4806,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| 843 | contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap); | 1066 | contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap); |
| 844 | contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props); | 1067 | contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props); |
| 845 | isReady = !hasUnknownTailSize; | 1068 | isReady = !hasUnknownTailSize; |
| ... | @@ -849,7 +1072,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -849,7 +1072,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 849 | } else if (anchorIndex >= 0) { | 1072 | } else if (anchorIndex >= 0) { |
| 850 | isReady = false; | 1073 | isReady = false; |
| 851 | } | 1074 | } |
| 852 | @@ -4671,6 +4819,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1075 | @@ -4671,6 +4827,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| 853 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); | 1076 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); |
| 854 | } | 1077 | } |
| 855 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | 1078 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); |
| ... | @@ -862,7 +1085,84 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -862,7 +1085,84 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 862 | } | 1085 | } |
| 863 | return nextSize; | 1086 | return nextSize; |
| 864 | } | 1087 | } |
| 865 | @@ -7054,6 +7208,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 1088 | @@ -5694,6 +5856,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ |
| 1089 | horizontal | ||
| 1090 | }) { | ||
| 1091 | const ctx = useStateContext(); | ||
| 1092 | + const SizeComponent = ctx.state.props.sizeComponentInternal; | ||
| 1093 | const columnWrapperStyle = ctx.columnWrapperStyle; | ||
| 1094 | const animSize = useValue$("totalSize"); | ||
| 1095 | const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]); | ||
| 1096 | @@ -5704,6 +5867,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ | ||
| 1097 | opacity: isVisible ? 1 : 0, | ||
| 1098 | width: animSize | ||
| 1099 | } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 }; | ||
| 1100 | + if (SizeComponent) { | ||
| 1101 | + if (horizontal) { | ||
| 1102 | + delete style.width; | ||
| 1103 | + } else { | ||
| 1104 | + delete style.height; | ||
| 1105 | + } | ||
| 1106 | + } | ||
| 1107 | if (columnWrapperStyle) { | ||
| 1108 | const { columnGap, rowGap, gap } = columnWrapperStyle; | ||
| 1109 | const gapX = columnGap || gap || 0; | ||
| 1110 | @@ -5724,7 +5894,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ | ||
| 1111 | } | ||
| 1112 | } | ||
| 1113 | } | ||
| 1114 | - return /* @__PURE__ */ React2.createElement(Animated.View, { pointerEvents: isVisible ? void 0 : "none", style }, /* @__PURE__ */ React2.createElement(ContainerLayoutCoordinator, null, children)); | ||
| 1115 | + const content = /* @__PURE__ */ React2.createElement(ContainerLayoutCoordinator, null, children); | ||
| 1116 | + return SizeComponent ? /* @__PURE__ */ React2.createElement(SizeComponent, { horizontal, pointerEvents: isVisible ? void 0 : "none", signalName: "totalSize", style }, content) : /* @__PURE__ */ React2.createElement(Animated.View, { pointerEvents: isVisible ? void 0 : "none", style }, content); | ||
| 1117 | }); | ||
| 1118 | var Containers = typedMemo(function Containers2({ | ||
| 1119 | freshDataTransitionEpoch, | ||
| 1120 | @@ -5875,7 +6046,12 @@ var StyleSheet = StyleSheet$1; | ||
| 1121 | |||
| 1122 | // src/components/ListComponent.tsx | ||
| 1123 | var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) { | ||
| 1124 | + const ctx = useStateContext(); | ||
| 1125 | const [alignItemsAtEndPadding = 0] = useArr$(["alignItemsAtEndPadding"]); | ||
| 1126 | + const SizeComponent = ctx.state.props.sizeComponentInternal; | ||
| 1127 | + if (SizeComponent) { | ||
| 1128 | + return /* @__PURE__ */ React2.createElement(SizeComponent, { horizontal, signalName: "alignItemsAtEndPadding", style: { flexShrink: 0 } }); | ||
| 1129 | + } | ||
| 1130 | if (alignItemsAtEndPadding <= 0) { | ||
| 1131 | return null; | ||
| 1132 | } | ||
| 1133 | @@ -5908,8 +6084,12 @@ var ListComponent = typedMemo(function ListComponent2({ | ||
| 1134 | refScrollView, | ||
| 1135 | renderScrollComponent, | ||
| 1136 | onLayoutFooter, | ||
| 1137 | + onInternalMomentumScrollBegin, | ||
| 1138 | onInternalScrollBeginDrag, | ||
| 1139 | + onInternalScrollEndDrag, | ||
| 1140 | onInternalScrollEnd, | ||
| 1141 | + onMomentumScrollBegin, | ||
| 1142 | + onScrollEndDrag, | ||
| 1143 | scrollAdjustHandler, | ||
| 1144 | snapToIndices, | ||
| 1145 | stickyHeaderConfig, | ||
| 1146 | @@ -5980,7 +6160,17 @@ var ListComponent = typedMemo(function ListComponent2({ | ||
| 1147 | SnapOrScroll, | ||
| 1148 | { | ||
| 1149 | ...rest, | ||
| 1150 | - ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, | ||
| 1151 | + ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { | ||
| 1152 | + onMomentumScrollBegin: (event) => { | ||
| 1153 | + onInternalMomentumScrollBegin == null ? void 0 : onInternalMomentumScrollBegin(event); | ||
| 1154 | + onMomentumScrollBegin == null ? void 0 : onMomentumScrollBegin(event); | ||
| 1155 | + }, | ||
| 1156 | + onScrollBeginDrag: onInternalScrollBeginDrag, | ||
| 1157 | + onScrollEndDrag: (event) => { | ||
| 1158 | + onInternalScrollEndDrag == null ? void 0 : onInternalScrollEndDrag(event); | ||
| 1159 | + onScrollEndDrag == null ? void 0 : onScrollEndDrag(event); | ||
| 1160 | + } | ||
| 1161 | + }, | ||
| 1162 | contentContainerStyle: [ | ||
| 1163 | horizontal ? { height: "100%" } : {}, | ||
| 1164 | contentContainerStyle, | ||
| 1165 | @@ -7054,6 +7244,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 866 | dataVersion, | 1166 | dataVersion, |
| 867 | drawDistance = 250, | 1167 | drawDistance = 250, |
| 868 | contentInsetEndAdjustment, | 1168 | contentInsetEndAdjustment, |
| ... | @@ -870,7 +1170,20 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -870,7 +1170,20 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 870 | estimatedItemSize = 100, | 1170 | estimatedItemSize = 100, |
| 871 | estimatedListSize, | 1171 | estimatedListSize, |
| 872 | extraData, | 1172 | extraData, |
| 873 | @@ -7179,7 +7334,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 1173 | @@ -7111,10 +7302,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 1174 | const animatedPropsInternal = props.animatedPropsInternal; | ||
| 1175 | const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list"; | ||
| 1176 | const positionComponentInternal = props.positionComponentInternal; | ||
| 1177 | + const sizeComponentInternal = props.sizeComponentInternal; | ||
| 1178 | const stickyPositionComponentInternal = props.stickyPositionComponentInternal; | ||
| 1179 | const { | ||
| 1180 | anchoredEndSpaceOwnerInternal: _anchoredEndSpaceOwnerInternal, | ||
| 1181 | positionComponentInternal: _positionComponentInternal, | ||
| 1182 | + sizeComponentInternal: _sizeComponentInternal, | ||
| 1183 | stickyPositionComponentInternal: _stickyPositionComponentInternal, | ||
| 1184 | ...restProps | ||
| 1185 | } = rest; | ||
| 1186 | @@ -7179,7 +7372,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 874 | const combinedRef = useCombinedRef(refScroller, refScrollView); | 1187 | const combinedRef = useCombinedRef(refScroller, refScrollView); |
| 875 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); | 1188 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); |
| 876 | const stickyHeaderIndices = stickyHeaderIndicesProp; | 1189 | const stickyHeaderIndices = stickyHeaderIndicesProp; |
| ... | @@ -879,7 +1192,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -879,7 +1192,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 879 | const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); | 1192 | const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); |
| 880 | const alwaysRenderIndices = useMemo(() => { | 1193 | const alwaysRenderIndices = useMemo(() => { |
| 881 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); | 1194 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); |
| 882 | @@ -7320,6 +7475,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 1195 | @@ -7320,6 +7513,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 883 | contentContainerAlignItems: contentContainerStyle.alignItems, | 1196 | contentContainerAlignItems: contentContainerStyle.alignItems, |
| 884 | contentInset, | 1197 | contentInset, |
| 885 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, | 1198 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, |
| ... | @@ -887,7 +1200,15 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -887,7 +1200,15 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 887 | data: dataProp, | 1200 | data: dataProp, |
| 888 | dataKey, | 1201 | dataKey, |
| 889 | dataVersion, | 1202 | dataVersion, |
| 890 | @@ -7402,6 +7558,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 1203 | @@ -7351,6 +7545,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 1204 | renderItem, | ||
| 1205 | rtl, | ||
| 1206 | snapToIndices, | ||
| 1207 | + sizeComponentInternal, | ||
| 1208 | stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [], | ||
| 1209 | stickyHeaderIndicesSet: useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]), | ||
| 1210 | stickyPositionComponentInternal, | ||
| 1211 | @@ -7402,6 +7597,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 891 | return void 0; | 1212 | return void 0; |
| 892 | } | 1213 | } |
| 893 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); | 1214 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); |
| ... | @@ -901,15 +1222,43 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -901,15 +1222,43 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 901 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; | 1222 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; |
| 902 | }, [usesBootstrapInitialScroll]); | 1223 | }, [usesBootstrapInitialScroll]); |
| 903 | useLayoutEffect(() => { | 1224 | useLayoutEffect(() => { |
| 904 | @@ -7630,6 +7793,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 1225 | @@ -7526,6 +7728,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 1226 | [ | ||
| 1227 | dataKey, | ||
| 1228 | dataVersion, | ||
| 1229 | + contentInsetStartAdjustment, | ||
| 1230 | memoizedLastItemKeys.join(","), | ||
| 1231 | numColumnsProp, | ||
| 1232 | nextScrollAxisGap, | ||
| 1233 | @@ -7622,6 +7825,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 1234 | () => ({ | ||
| 1235 | getRenderedItem: (key) => getRenderedItem(ctx, key), | ||
| 1236 | onMomentumScrollEnd: (event) => { | ||
| 1237 | + state.isMomentumScrolling = false; | ||
| 1238 | checkFinishedScrollFallback(ctx); | ||
| 1239 | if (state.props.onMomentumScrollEnd) { | ||
| 1240 | state.props.onMomentumScrollEnd(event); | ||
| 1241 | @@ -7630,6 +7834,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 905 | onScroll: (event) => onScroll(ctx, event), | 1242 | onScroll: (event) => onScroll(ctx, event), |
| 906 | onScrollBeginDrag: (event) => { | 1243 | onScrollBeginDrag: (event) => { |
| 907 | var _a4, _b2; | 1244 | var _a4, _b2; |
| 908 | + ctx.state.didUserDrag = true; | 1245 | + ctx.state.didUserDrag = true; |
| 1246 | + ctx.state.isUserDragging = true; | ||
| 909 | prepareReachedEdgeForNextUserScroll(ctx); | 1247 | prepareReachedEdgeForNextUserScroll(ctx); |
| 910 | (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); | 1248 | (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); |
| 911 | }, | 1249 | }, |
| 912 | @@ -7660,6 +7824,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | 1250 | @@ -7655,11 +7861,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded |
| 1251 | ListFooterComponent, | ||
| 1252 | ListFooterComponentStyle, | ||
| 1253 | ListHeaderComponent, | ||
| 1254 | + onInternalMomentumScrollBegin: () => { | ||
| 1255 | + ctx.state.isMomentumScrolling = true; | ||
| 1256 | + }, | ||
| 1257 | onInternalScrollBeginDrag: fns.onScrollBeginDrag, | ||
| 1258 | + onInternalScrollEndDrag: () => { | ||
| 1259 | + ctx.state.isUserDragging = false; | ||
| 1260 | + }, | ||
| 1261 | onInternalScrollEnd: fns.onScrollEnd, | ||
| 913 | onLayout, | 1262 | onLayout, |
| 914 | onLayoutFooter, | 1263 | onLayoutFooter, |
| 915 | onMomentumScrollEnd: fns.onMomentumScrollEnd, | 1264 | onMomentumScrollEnd: fns.onMomentumScrollEnd, |
| ... | @@ -918,7 +1267,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c | ... | @@ -918,7 +1267,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..2d556eecb0bafe2f54084c809f82144c |
| 918 | recycleItems, | 1267 | recycleItems, |
| 919 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { | 1268 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { |
| 920 | diff --git a/react.js b/react.js | 1269 | diff --git a/react.js b/react.js |
| 921 | index 914d2dafaafa001c9ab6791a0a0581659295e333..06466096ceb96ef8773f7f7e202a19c9b774d49b 100644 | 1270 | index 914d2dafaafa001c9ab6791a0a0581659295e333..a2df18d450eebe451fda42bab47d1658458edbb1 100644 |
| 922 | --- a/react.js | 1271 | --- a/react.js |
| 923 | +++ b/react.js | 1272 | +++ b/react.js |
| 924 | @@ -4702,7 +4702,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1273 | @@ -4702,7 +4702,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| ... | @@ -931,8 +1280,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..06466096ceb96ef8773f7f7e202a19c9 | ... | @@ -931,8 +1280,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..06466096ceb96ef8773f7f7e202a19c9 |
| 931 | } else if (anchorIndex >= 0) { | 1280 | } else if (anchorIndex >= 0) { |
| 932 | isReady = false; | 1281 | isReady = false; |
| 933 | } | 1282 | } |
| 934 | @@ -4721,7 +4722,13 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1283 | @@ -4722,6 +4723,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| 935 | updateContentMetricsState(ctx); | ||
| 936 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); | 1284 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); |
| 937 | } | 1285 | } |
| 938 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | 1286 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); |
| ... | @@ -945,7 +1293,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..06466096ceb96ef8773f7f7e202a19c9 | ... | @@ -945,7 +1293,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..06466096ceb96ef8773f7f7e202a19c9 |
| 945 | } | 1293 | } |
| 946 | return nextSize; | 1294 | return nextSize; |
| 947 | } | 1295 | } |
| 948 | @@ -6448,8 +6455,8 @@ function ScrollAdjust() { | 1296 | @@ -6446,8 +6453,8 @@ function ScrollAdjust() { |
| 949 | window.getComputedStyle(contentNode)[axis.paddingEndProp] | 1297 | window.getComputedStyle(contentNode)[axis.paddingEndProp] |
| 950 | ); | 1298 | ); |
| 951 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; | 1299 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; |
| ... | @@ -956,7 +1304,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..06466096ceb96ef8773f7f7e202a19c9 | ... | @@ -956,7 +1304,7 @@ index 914d2dafaafa001c9ab6791a0a0581659295e333..06466096ceb96ef8773f7f7e202a19c9 |
| 956 | scrollBy(); | 1304 | scrollBy(); |
| 957 | if (resetPaddingRafRef.current !== void 0) { | 1305 | if (resetPaddingRafRef.current !== void 0) { |
| 958 | diff --git a/react.mjs b/react.mjs | 1306 | diff --git a/react.mjs b/react.mjs |
| 959 | index 95465f2ab89ce41a10553f58af83618f7310e83c..4b5f06c60f1e8bbd9035a49980d9e86820262da4 100644 | 1307 | index 95465f2ab89ce41a10553f58af83618f7310e83c..25cf046f2c3141ddce5a0b6e28c354b865331b4f 100644 |
| 960 | --- a/react.mjs | 1308 | --- a/react.mjs |
| 961 | +++ b/react.mjs | 1309 | +++ b/react.mjs |
| 962 | @@ -4681,7 +4681,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1310 | @@ -4681,7 +4681,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| ... | @@ -969,8 +1317,7 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..4b5f06c60f1e8bbd9035a49980d9e868 | ... | @@ -969,8 +1317,7 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..4b5f06c60f1e8bbd9035a49980d9e868 |
| 969 | } else if (anchorIndex >= 0) { | 1317 | } else if (anchorIndex >= 0) { |
| 970 | isReady = false; | 1318 | isReady = false; |
| 971 | } | 1319 | } |
| 972 | @@ -4700,7 +4701,13 @@ function maybeUpdateAnchoredEndSpace(ctx) { | 1320 | @@ -4701,6 +4702,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { |
| 973 | updateContentMetricsState(ctx); | ||
| 974 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); | 1321 | updateScroll(ctx, state.scroll, true, { markHasScrolled: false }); |
| 975 | } | 1322 | } |
| 976 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | 1323 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); |
| ... | @@ -983,7 +1330,7 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..4b5f06c60f1e8bbd9035a49980d9e868 | ... | @@ -983,7 +1330,7 @@ index 95465f2ab89ce41a10553f58af83618f7310e83c..4b5f06c60f1e8bbd9035a49980d9e868 |
| 983 | } | 1330 | } |
| 984 | return nextSize; | 1331 | return nextSize; |
| 985 | } | 1332 | } |
| 986 | @@ -6427,8 +6434,8 @@ function ScrollAdjust() { | 1333 | @@ -6425,8 +6432,8 @@ function ScrollAdjust() { |
| 987 | window.getComputedStyle(contentNode)[axis.paddingEndProp] | 1334 | window.getComputedStyle(contentNode)[axis.paddingEndProp] |
| 988 | ); | 1335 | ); |
| 989 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; | 1336 | const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; |
| ... | @@ -1011,19 +1358,22 @@ index e5043320700b12f34f4c0babbc341f85ca8135c1..2ce63830a28636b21937a0741fd0e613 | ... | @@ -1011,19 +1358,22 @@ index e5043320700b12f34f4c0babbc341f85ca8135c1..2ce63830a28636b21937a0741fd0e613 |
| 1011 | * Number of columns to render items in. | 1358 | * Number of columns to render items in. |
| 1012 | * @default 1 | 1359 | * @default 1 |
| 1013 | diff --git a/reanimated.js b/reanimated.js | 1360 | diff --git a/reanimated.js b/reanimated.js |
| 1014 | index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..16dcef04a6591d500c724635df272274123bad2d 100644 | 1361 | index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71ff8ce9f4d 100644 |
| 1015 | --- a/reanimated.js | 1362 | --- a/reanimated.js |
| 1016 | +++ b/reanimated.js | 1363 | +++ b/reanimated.js |
| 1017 | @@ -116,7 +116,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | 1364 | @@ -115,8 +115,10 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( |
| 1365 | const { id, horizontal, style, refView, children, recycleItems, layoutTransition, ...rest } = props; | ||
| 1018 | const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); | 1366 | const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); |
| 1019 | const prevItemKeyRef = React__namespace.useRef(void 0); | 1367 | const prevItemKeyRef = React__namespace.useRef(void 0); |
| 1368 | + const previousPositionRef = React__namespace.useRef(positionValue); | ||
| 1369 | + const lastContentSizeEpochRef = React__namespace.useRef(ctx.state.contentSizeAnimationEpoch || 0); | ||
| 1020 | let shouldSkipTransitionForRecycleReuse = false; | 1370 | let shouldSkipTransitionForRecycleReuse = false; |
| 1021 | - if (recycleItems && layoutTransition) { | 1371 | - if (recycleItems && layoutTransition) { |
| 1022 | + if (layoutTransition) { | 1372 | + if (layoutTransition) { |
| 1023 | const itemKeySignal = `containerItemKey${id}`; | 1373 | const itemKeySignal = `containerItemKey${id}`; |
| 1024 | const itemKey = peek$(ctx, itemKeySignal); | 1374 | const itemKey = peek$(ctx, itemKeySignal); |
| 1025 | shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; | 1375 | shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; |
| 1026 | @@ -130,10 +130,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | 1376 | @@ -130,10 +132,29 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( |
| 1027 | () => [style, horizontal ? { left: positionValue } : { top: positionValue }], | 1377 | () => [style, horizontal ? { left: positionValue } : { top: positionValue }], |
| 1028 | [horizontal, positionValue, style] | 1378 | [horizontal, positionValue, style] |
| 1029 | ); | 1379 | ); |
| ... | @@ -1035,30 +1385,168 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..16dcef04a6591d500c724635df272274 | ... | @@ -1035,30 +1385,168 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..16dcef04a6591d500c724635df272274 |
| 1035 | + // transition exists to smooth at-rest shifts (streaming text, work-log | 1385 | + // transition exists to smooth at-rest shifts (streaming text, work-log |
| 1036 | + // folds), so gate it to at-rest moments. | 1386 | + // folds), so gate it to at-rest moments. |
| 1037 | + const now = Date.now(); | 1387 | + const now = Date.now(); |
| 1038 | + const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || | 1388 | + const contentSizeEpoch = ctx.state.contentSizeAnimationEpoch || 0; |
| 1039 | + now - (ctx.state.lastNativeScrollTime || 0) < 300; | 1389 | + const didPositionChange = positionValue !== previousPositionRef.current; |
| 1390 | + const isRawSizeReposition = didPositionChange && contentSizeEpoch !== lastContentSizeEpochRef.current && ctx.state.contentSizeAnimationEligible && !!layoutTransition && typeof layoutTransition.getAnimationAndConfig === "function"; | ||
| 1391 | + React__namespace.useLayoutEffect(() => { | ||
| 1392 | + if (didPositionChange) { | ||
| 1393 | + previousPositionRef.current = positionValue; | ||
| 1394 | + lastContentSizeEpochRef.current = contentSizeEpoch; | ||
| 1395 | + } | ||
| 1396 | + }, [contentSizeEpoch, didPositionChange, positionValue]); | ||
| 1397 | + const isRecentNativeReposition = now - (ctx.state.lastNativeScrollTime || 0) < 300; | ||
| 1398 | + const shouldSkipLayoutTransition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || ctx.state.isUserDragging || ctx.state.isMomentumScrolling || isRecentNativeReposition && !isRawSizeReposition; | ||
| 1040 | return /* @__PURE__ */ React__namespace.createElement( | 1399 | return /* @__PURE__ */ React__namespace.createElement( |
| 1041 | Reanimated__default.default.View, | 1400 | Reanimated__default.default.View, |
| 1042 | { | 1401 | { |
| 1043 | - layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, | 1402 | - layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, |
| 1044 | + layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, | 1403 | + layout: shouldSkipTransitionForRecycleReuse || shouldSkipLayoutTransition ? void 0 : layoutTransition, |
| 1045 | ref: refView, | 1404 | ref: refView, |
| 1046 | style: viewStyle, | 1405 | style: viewStyle, |
| 1047 | ...rest | 1406 | ...rest |
| 1407 | @@ -141,6 +162,96 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | ||
| 1408 | children | ||
| 1409 | ); | ||
| 1410 | }); | ||
| 1411 | +var ReanimatedSizeView = typedMemo(function ReanimatedSizeViewComponent(props) { | ||
| 1412 | + const ctx = useStateContext(); | ||
| 1413 | + const { horizontal, signalName, style, children, ...rest } = props; | ||
| 1414 | + const [logicalSize = 0] = useArr$(signalName === "totalSize" ? [signalName] : [signalName, "totalSize"]); | ||
| 1415 | + const baseSize = Reanimated.useSharedValue(logicalSize); | ||
| 1416 | + const animatedDelta = Reanimated.useSharedValue(0); | ||
| 1417 | + const previousLogicalSizeRef = React__namespace.useRef(logicalSize); | ||
| 1418 | + const previousEpochRef = React__namespace.useRef(ctx.state.contentSizeAnimationEpoch || 0); | ||
| 1419 | + const animationRunRef = React__namespace.useRef(0); | ||
| 1420 | + const isAnimatingRef = React__namespace.useRef(false); | ||
| 1421 | + const [isAnimating, setIsAnimating] = React__namespace.useState(false); | ||
| 1422 | + const transition = props.layoutTransition; | ||
| 1423 | + const canAnimate = !!transition && typeof transition.getAnimationAndConfig === "function"; | ||
| 1424 | + const renderEpoch = ctx.state.contentSizeAnimationEpoch || 0; | ||
| 1425 | + const startsEligibleAnimation = canAnimate && logicalSize !== previousLogicalSizeRef.current && renderEpoch !== previousEpochRef.current && ctx.state.contentSizeAnimationEligible; | ||
| 1426 | + const useAnimatedAxis = isAnimating || startsEligibleAnimation; | ||
| 1427 | + const animatedStyle = Reanimated.useAnimatedStyle(() => { | ||
| 1428 | + const size = Math.max(0, baseSize.value + animatedDelta.value); | ||
| 1429 | + return horizontal ? { width: size } : { height: size }; | ||
| 1430 | + }, [horizontal]); | ||
| 1431 | + const completeAnimation = React__namespace.useCallback((run, epoch) => { | ||
| 1432 | + if (animationRunRef.current !== run) { | ||
| 1433 | + return; | ||
| 1434 | + } | ||
| 1435 | + isAnimatingRef.current = false; | ||
| 1436 | + setIsAnimating(false); | ||
| 1437 | + const state = ctx.state; | ||
| 1438 | + if (state.contentSizeAnimationActiveEpoch !== epoch) { | ||
| 1439 | + return; | ||
| 1440 | + } | ||
| 1441 | + state.contentSizeAnimationActiveSignals.delete(signalName); | ||
| 1442 | + if (state.contentSizeAnimationActiveSignals.size === 0) { | ||
| 1443 | + state.contentSizeAnimationActiveEpoch = void 0; | ||
| 1444 | + state.contentSizeAnimationEligible = false; | ||
| 1445 | + } | ||
| 1446 | + }, [ctx.state, signalName]); | ||
| 1447 | + React__namespace.useLayoutEffect(() => { | ||
| 1448 | + const state = ctx.state; | ||
| 1449 | + const epoch = state.contentSizeAnimationEpoch || 0; | ||
| 1450 | + const previousLogicalSize = previousLogicalSizeRef.current; | ||
| 1451 | + const delta = logicalSize - previousLogicalSize; | ||
| 1452 | + const isNewEpoch = epoch !== previousEpochRef.current; | ||
| 1453 | + const isNewEligibleEpoch = canAnimate && isNewEpoch && state.contentSizeAnimationEligible; | ||
| 1454 | + previousLogicalSizeRef.current = logicalSize; | ||
| 1455 | + previousEpochRef.current = epoch; | ||
| 1456 | + if (!delta) { | ||
| 1457 | + return; | ||
| 1458 | + } | ||
| 1459 | + if (!isNewEligibleEpoch) { | ||
| 1460 | + baseSize.value = logicalSize; | ||
| 1461 | + if (signalName === "totalSize" && isNewEpoch && state.contentSizeAnimationEligible && !canAnimate) { | ||
| 1462 | + state.contentSizeAnimationEligible = false; | ||
| 1463 | + } | ||
| 1464 | + return; | ||
| 1465 | + } | ||
| 1466 | + const [animation, config] = transition.getAnimationAndConfig(); | ||
| 1467 | + const delayFunction = typeof transition.getDelayFunction === "function" ? transition.getDelayFunction() : (_, value) => value; | ||
| 1468 | + const delay = typeof transition.getDelay === "function" ? transition.getDelay() : 0; | ||
| 1469 | + if (typeof animation !== "function" || typeof delayFunction !== "function") { | ||
| 1470 | + baseSize.value = logicalSize; | ||
| 1471 | + if (signalName === "totalSize") { | ||
| 1472 | + state.contentSizeAnimationEligible = false; | ||
| 1473 | + } | ||
| 1474 | + return; | ||
| 1475 | + } | ||
| 1476 | + if (state.contentSizeAnimationActiveEpoch !== epoch) { | ||
| 1477 | + state.contentSizeAnimationActiveEpoch = epoch; | ||
| 1478 | + state.contentSizeAnimationActiveSignals = /* @__PURE__ */ new Set(); | ||
| 1479 | + } | ||
| 1480 | + state.contentSizeAnimationActiveSignals.add(signalName); | ||
| 1481 | + const wasAnimating = isAnimatingRef.current; | ||
| 1482 | + isAnimatingRef.current = true; | ||
| 1483 | + setIsAnimating(true); | ||
| 1484 | + const run = ++animationRunRef.current; | ||
| 1485 | + Reanimated.runOnUI((absoluteSize, previousSize, continuesAnimation, animationRun, animationEpoch) => { | ||
| 1486 | + "worklet"; | ||
| 1487 | + const currentVisualSize = continuesAnimation ? baseSize.value + animatedDelta.value : previousSize; | ||
| 1488 | + baseSize.value = absoluteSize; | ||
| 1489 | + animatedDelta.value = currentVisualSize - absoluteSize; | ||
| 1490 | + animatedDelta.value = delayFunction(delay, animation(0, config, (finished) => { | ||
| 1491 | + "worklet"; | ||
| 1492 | + if (finished) { | ||
| 1493 | + Reanimated.runOnJS(completeAnimation)(animationRun, animationEpoch); | ||
| 1494 | + } | ||
| 1495 | + })); | ||
| 1496 | + })(logicalSize, previousLogicalSize, wasAnimating, run, epoch); | ||
| 1497 | + }, [baseSize, animatedDelta, canAnimate, completeAnimation, ctx.state, logicalSize, renderEpoch, signalName, transition]); | ||
| 1498 | + const sizeStyle = useAnimatedAxis ? animatedStyle : horizontal ? { width: logicalSize } : { height: logicalSize }; | ||
| 1499 | + return /* @__PURE__ */ React__namespace.createElement(Reanimated__default.default.View, { ...rest, style: [style, sizeStyle] }, children); | ||
| 1500 | +}); | ||
| 1501 | function setSharedValueValue(sharedValue, value) { | ||
| 1502 | if (!sharedValue) { | ||
| 1503 | return; | ||
| 1504 | @@ -245,10 +356,19 @@ var LegendListForwardedRef = typedMemo( | ||
| 1505 | ); | ||
| 1506 | }; | ||
| 1507 | }, [hasItemLayoutAnimation, recycleItems]); | ||
| 1508 | + const sizeComponentInternal = React__namespace.useMemo(() => { | ||
| 1509 | + if (!hasItemLayoutAnimation) { | ||
| 1510 | + return void 0; | ||
| 1511 | + } | ||
| 1512 | + return function SizeComponent(sizeProps) { | ||
| 1513 | + return /* @__PURE__ */ React__namespace.createElement(ReanimatedSizeView, { ...sizeProps, layoutTransition: itemLayoutAnimationRef.current }); | ||
| 1514 | + }; | ||
| 1515 | + }, [hasItemLayoutAnimation]); | ||
| 1516 | const legendListProps = { | ||
| 1517 | ...rest, | ||
| 1518 | positionComponentInternal, | ||
| 1519 | recycleItems, | ||
| 1520 | + sizeComponentInternal, | ||
| 1521 | ...{ | ||
| 1522 | renderScrollComponent: renderReanimatedScrollComponent, | ||
| 1523 | ...IsNewArchitecture ? { stickyPositionComponentInternal } : {} | ||
| 1048 | diff --git a/reanimated.mjs b/reanimated.mjs | 1524 | diff --git a/reanimated.mjs b/reanimated.mjs |
| 1049 | index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..9c25ec5dd79fb137c073adc24643ad8a2996bf56 100644 | 1525 | index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a5725398942 100644 |
| 1050 | --- a/reanimated.mjs | 1526 | --- a/reanimated.mjs |
| 1051 | +++ b/reanimated.mjs | 1527 | +++ b/reanimated.mjs |
| 1052 | @@ -92,7 +92,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | 1528 | @@ -1,7 +1,7 @@ |
| 1529 | import * as React from 'react'; | ||
| 1530 | import { useCallback } from 'react'; | ||
| 1531 | import { View } from 'react-native'; | ||
| 1532 | -import Reanimated, { useAnimatedRef, useAnimatedStyle, useSharedValue, useScrollViewOffset } from 'react-native-reanimated'; | ||
| 1533 | +import Reanimated, { runOnJS, runOnUI, useAnimatedRef, useAnimatedStyle, useSharedValue, useScrollViewOffset } from 'react-native-reanimated'; | ||
| 1534 | import { internal, LegendList } from '@legendapp/list/react-native'; | ||
| 1535 | |||
| 1536 | // src/integrations/reanimated.tsx | ||
| 1537 | @@ -91,8 +91,10 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | ||
| 1538 | const { id, horizontal, style, refView, children, recycleItems, layoutTransition, ...rest } = props; | ||
| 1053 | const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); | 1539 | const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); |
| 1054 | const prevItemKeyRef = React.useRef(void 0); | 1540 | const prevItemKeyRef = React.useRef(void 0); |
| 1541 | + const previousPositionRef = React.useRef(positionValue); | ||
| 1542 | + const lastContentSizeEpochRef = React.useRef(ctx.state.contentSizeAnimationEpoch || 0); | ||
| 1055 | let shouldSkipTransitionForRecycleReuse = false; | 1543 | let shouldSkipTransitionForRecycleReuse = false; |
| 1056 | - if (recycleItems && layoutTransition) { | 1544 | - if (recycleItems && layoutTransition) { |
| 1057 | + if (layoutTransition) { | 1545 | + if (layoutTransition) { |
| 1058 | const itemKeySignal = `containerItemKey${id}`; | 1546 | const itemKeySignal = `containerItemKey${id}`; |
| 1059 | const itemKey = peek$(ctx, itemKeySignal); | 1547 | const itemKey = peek$(ctx, itemKeySignal); |
| 1060 | shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; | 1548 | shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; |
| 1061 | @@ -106,10 +106,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | 1549 | @@ -106,10 +108,29 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( |
| 1062 | () => [style, horizontal ? { left: positionValue } : { top: positionValue }], | 1550 | () => [style, horizontal ? { left: positionValue } : { top: positionValue }], |
| 1063 | [horizontal, positionValue, style] | 1551 | [horizontal, positionValue, style] |
| 1064 | ); | 1552 | ); |
| ... | @@ -1070,13 +1558,139 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..9c25ec5dd79fb137c073adc24643ad8a | ... | @@ -1070,13 +1558,139 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..9c25ec5dd79fb137c073adc24643ad8a |
| 1070 | + // transition exists to smooth at-rest shifts (streaming text, work-log | 1558 | + // transition exists to smooth at-rest shifts (streaming text, work-log |
| 1071 | + // folds), so gate it to at-rest moments. | 1559 | + // folds), so gate it to at-rest moments. |
| 1072 | + const now = Date.now(); | 1560 | + const now = Date.now(); |
| 1073 | + const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || | 1561 | + const contentSizeEpoch = ctx.state.contentSizeAnimationEpoch || 0; |
| 1074 | + now - (ctx.state.lastNativeScrollTime || 0) < 300; | 1562 | + const didPositionChange = positionValue !== previousPositionRef.current; |
| 1563 | + const isRawSizeReposition = didPositionChange && contentSizeEpoch !== lastContentSizeEpochRef.current && ctx.state.contentSizeAnimationEligible && !!layoutTransition && typeof layoutTransition.getAnimationAndConfig === "function"; | ||
| 1564 | + React.useLayoutEffect(() => { | ||
| 1565 | + if (didPositionChange) { | ||
| 1566 | + previousPositionRef.current = positionValue; | ||
| 1567 | + lastContentSizeEpochRef.current = contentSizeEpoch; | ||
| 1568 | + } | ||
| 1569 | + }, [contentSizeEpoch, didPositionChange, positionValue]); | ||
| 1570 | + const isRecentNativeReposition = now - (ctx.state.lastNativeScrollTime || 0) < 300; | ||
| 1571 | + const shouldSkipLayoutTransition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || ctx.state.isUserDragging || ctx.state.isMomentumScrolling || isRecentNativeReposition && !isRawSizeReposition; | ||
| 1075 | return /* @__PURE__ */ React.createElement( | 1572 | return /* @__PURE__ */ React.createElement( |
| 1076 | Reanimated.View, | 1573 | Reanimated.View, |
| 1077 | { | 1574 | { |
| 1078 | - layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, | 1575 | - layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, |
| 1079 | + layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, | 1576 | + layout: shouldSkipTransitionForRecycleReuse || shouldSkipLayoutTransition ? void 0 : layoutTransition, |
| 1080 | ref: refView, | 1577 | ref: refView, |
| 1081 | style: viewStyle, | 1578 | style: viewStyle, |
| 1082 | ...rest | 1579 | ...rest |
| 1580 | @@ -117,6 +138,96 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | ||
| 1581 | children | ||
| 1582 | ); | ||
| 1583 | }); | ||
| 1584 | +var ReanimatedSizeView = typedMemo(function ReanimatedSizeViewComponent(props) { | ||
| 1585 | + const ctx = useStateContext(); | ||
| 1586 | + const { horizontal, signalName, style, children, ...rest } = props; | ||
| 1587 | + const [logicalSize = 0] = useArr$(signalName === "totalSize" ? [signalName] : [signalName, "totalSize"]); | ||
| 1588 | + const baseSize = useSharedValue(logicalSize); | ||
| 1589 | + const animatedDelta = useSharedValue(0); | ||
| 1590 | + const previousLogicalSizeRef = React.useRef(logicalSize); | ||
| 1591 | + const previousEpochRef = React.useRef(ctx.state.contentSizeAnimationEpoch || 0); | ||
| 1592 | + const animationRunRef = React.useRef(0); | ||
| 1593 | + const isAnimatingRef = React.useRef(false); | ||
| 1594 | + const [isAnimating, setIsAnimating] = React.useState(false); | ||
| 1595 | + const transition = props.layoutTransition; | ||
| 1596 | + const canAnimate = !!transition && typeof transition.getAnimationAndConfig === "function"; | ||
| 1597 | + const renderEpoch = ctx.state.contentSizeAnimationEpoch || 0; | ||
| 1598 | + const startsEligibleAnimation = canAnimate && logicalSize !== previousLogicalSizeRef.current && renderEpoch !== previousEpochRef.current && ctx.state.contentSizeAnimationEligible; | ||
| 1599 | + const useAnimatedAxis = isAnimating || startsEligibleAnimation; | ||
| 1600 | + const animatedStyle = useAnimatedStyle(() => { | ||
| 1601 | + const size = Math.max(0, baseSize.value + animatedDelta.value); | ||
| 1602 | + return horizontal ? { width: size } : { height: size }; | ||
| 1603 | + }, [horizontal]); | ||
| 1604 | + const completeAnimation = useCallback((run, epoch) => { | ||
| 1605 | + if (animationRunRef.current !== run) { | ||
| 1606 | + return; | ||
| 1607 | + } | ||
| 1608 | + isAnimatingRef.current = false; | ||
| 1609 | + setIsAnimating(false); | ||
| 1610 | + const state = ctx.state; | ||
| 1611 | + if (state.contentSizeAnimationActiveEpoch !== epoch) { | ||
| 1612 | + return; | ||
| 1613 | + } | ||
| 1614 | + state.contentSizeAnimationActiveSignals.delete(signalName); | ||
| 1615 | + if (state.contentSizeAnimationActiveSignals.size === 0) { | ||
| 1616 | + state.contentSizeAnimationActiveEpoch = void 0; | ||
| 1617 | + state.contentSizeAnimationEligible = false; | ||
| 1618 | + } | ||
| 1619 | + }, [ctx.state, signalName]); | ||
| 1620 | + React.useLayoutEffect(() => { | ||
| 1621 | + const state = ctx.state; | ||
| 1622 | + const epoch = state.contentSizeAnimationEpoch || 0; | ||
| 1623 | + const previousLogicalSize = previousLogicalSizeRef.current; | ||
| 1624 | + const delta = logicalSize - previousLogicalSize; | ||
| 1625 | + const isNewEpoch = epoch !== previousEpochRef.current; | ||
| 1626 | + const isNewEligibleEpoch = canAnimate && isNewEpoch && state.contentSizeAnimationEligible; | ||
| 1627 | + previousLogicalSizeRef.current = logicalSize; | ||
| 1628 | + previousEpochRef.current = epoch; | ||
| 1629 | + if (!delta) { | ||
| 1630 | + return; | ||
| 1631 | + } | ||
| 1632 | + if (!isNewEligibleEpoch) { | ||
| 1633 | + baseSize.value = logicalSize; | ||
| 1634 | + if (signalName === "totalSize" && isNewEpoch && state.contentSizeAnimationEligible && !canAnimate) { | ||
| 1635 | + state.contentSizeAnimationEligible = false; | ||
| 1636 | + } | ||
| 1637 | + return; | ||
| 1638 | + } | ||
| 1639 | + const [animation, config] = transition.getAnimationAndConfig(); | ||
| 1640 | + const delayFunction = typeof transition.getDelayFunction === "function" ? transition.getDelayFunction() : (_, value) => value; | ||
| 1641 | + const delay = typeof transition.getDelay === "function" ? transition.getDelay() : 0; | ||
| 1642 | + if (typeof animation !== "function" || typeof delayFunction !== "function") { | ||
| 1643 | + baseSize.value = logicalSize; | ||
| 1644 | + if (signalName === "totalSize") { | ||
| 1645 | + state.contentSizeAnimationEligible = false; | ||
| 1646 | + } | ||
| 1647 | + return; | ||
| 1648 | + } | ||
| 1649 | + if (state.contentSizeAnimationActiveEpoch !== epoch) { | ||
| 1650 | + state.contentSizeAnimationActiveEpoch = epoch; | ||
| 1651 | + state.contentSizeAnimationActiveSignals = /* @__PURE__ */ new Set(); | ||
| 1652 | + } | ||
| 1653 | + state.contentSizeAnimationActiveSignals.add(signalName); | ||
| 1654 | + const wasAnimating = isAnimatingRef.current; | ||
| 1655 | + isAnimatingRef.current = true; | ||
| 1656 | + setIsAnimating(true); | ||
| 1657 | + const run = ++animationRunRef.current; | ||
| 1658 | + runOnUI((absoluteSize, previousSize, continuesAnimation, animationRun, animationEpoch) => { | ||
| 1659 | + "worklet"; | ||
| 1660 | + const currentVisualSize = continuesAnimation ? baseSize.value + animatedDelta.value : previousSize; | ||
| 1661 | + baseSize.value = absoluteSize; | ||
| 1662 | + animatedDelta.value = currentVisualSize - absoluteSize; | ||
| 1663 | + animatedDelta.value = delayFunction(delay, animation(0, config, (finished) => { | ||
| 1664 | + "worklet"; | ||
| 1665 | + if (finished) { | ||
| 1666 | + runOnJS(completeAnimation)(animationRun, animationEpoch); | ||
| 1667 | + } | ||
| 1668 | + })); | ||
| 1669 | + })(logicalSize, previousLogicalSize, wasAnimating, run, epoch); | ||
| 1670 | + }, [baseSize, animatedDelta, canAnimate, completeAnimation, ctx.state, logicalSize, renderEpoch, signalName, transition]); | ||
| 1671 | + const sizeStyle = useAnimatedAxis ? animatedStyle : horizontal ? { width: logicalSize } : { height: logicalSize }; | ||
| 1672 | + return /* @__PURE__ */ React.createElement(Reanimated.View, { ...rest, style: [style, sizeStyle] }, children); | ||
| 1673 | +}); | ||
| 1674 | function setSharedValueValue(sharedValue, value) { | ||
| 1675 | if (!sharedValue) { | ||
| 1676 | return; | ||
| 1677 | @@ -221,10 +332,19 @@ var LegendListForwardedRef = typedMemo( | ||
| 1678 | ); | ||
| 1679 | }; | ||
| 1680 | }, [hasItemLayoutAnimation, recycleItems]); | ||
| 1681 | + const sizeComponentInternal = React.useMemo(() => { | ||
| 1682 | + if (!hasItemLayoutAnimation) { | ||
| 1683 | + return void 0; | ||
| 1684 | + } | ||
| 1685 | + return function SizeComponent(sizeProps) { | ||
| 1686 | + return /* @__PURE__ */ React.createElement(ReanimatedSizeView, { ...sizeProps, layoutTransition: itemLayoutAnimationRef.current }); | ||
| 1687 | + }; | ||
| 1688 | + }, [hasItemLayoutAnimation]); | ||
| 1689 | const legendListProps = { | ||
| 1690 | ...rest, | ||
| 1691 | positionComponentInternal, | ||
| 1692 | recycleItems, | ||
| 1693 | + sizeComponentInternal, | ||
| 1694 | ...{ | ||
| 1695 | renderScrollComponent: renderReanimatedScrollComponent, | ||
| 1696 | ...IsNewArchitecture ? { stickyPositionComponentInternal } : {} |
patches/react-native-keyboard-controller@1.21.13.patch+456-70| ... | @@ -1,12 +1,13 @@ | ... | @@ -1,12 +1,13 @@ |
| 1 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/index.js b/lib/commonjs/components/KeyboardChatScrollView/index.js | 1 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/index.js b/lib/commonjs/components/KeyboardChatScrollView/index.js |
| 2 | index db8cfb1d289f91563f13c4dd842c783c99facc32..940e1dd80c6c9dfc42eab916445be414372ce52e 100644 | 2 | index db8cfb1d289f91563f13c4dd842c783c99facc32..64c7e38ad8d9e866b15a53e561509e6b0ad33cb9 100644 |
| 3 | --- a/lib/commonjs/components/KeyboardChatScrollView/index.js | 3 | --- a/lib/commonjs/components/KeyboardChatScrollView/index.js |
| 4 | +++ b/lib/commonjs/components/KeyboardChatScrollView/index.js | 4 | +++ b/lib/commonjs/components/KeyboardChatScrollView/index.js |
| 5 | @@ -26,9 +26,11 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | 5 | @@ -26,9 +26,12 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ |
| 6 | offset = 0, | 6 | offset = 0, |
| 7 | extraContentPadding = ZERO_CONTENT_PADDING, | 7 | extraContentPadding = ZERO_CONTENT_PADDING, |
| 8 | blankSpace = ZERO_BLANK_SPACE, | 8 | blankSpace = ZERO_BLANK_SPACE, |
| 9 | + adjustedInsetCompensation = 0, | 9 | + adjustedInsetCompensation = 0, |
| 10 | + adjustedStartInsetCompensation = 0, | ||
| 10 | applyWorkaroundForContentInsetHitTestBug = false, | 11 | applyWorkaroundForContentInsetHitTestBug = false, |
| 11 | onLayout: onLayoutProp, | 12 | onLayout: onLayoutProp, |
| 12 | onContentSizeChange: onContentSizeChangeProp, | 13 | onContentSizeChange: onContentSizeChangeProp, |
| ... | @@ -14,13 +15,14 @@ index db8cfb1d289f91563f13c4dd842c783c99facc32..940e1dd80c6c9dfc42eab916445be414 | ... | @@ -14,13 +15,14 @@ index db8cfb1d289f91563f13c4dd842c783c99facc32..940e1dd80c6c9dfc42eab916445be414 |
| 14 | onEndVisible, | 15 | onEndVisible, |
| 15 | ...rest | 16 | ...rest |
| 16 | }, ref) => { | 17 | }, ref) => { |
| 17 | @@ -50,13 +52,15 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | 18 | @@ -50,13 +53,17 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ |
| 18 | freeze: freezeSV, | 19 | freeze: freezeSV, |
| 19 | offset, | 20 | offset, |
| 20 | blankSpace, | 21 | blankSpace, |
| 21 | - extraContentPadding | 22 | - extraContentPadding |
| 22 | + extraContentPadding, | 23 | + extraContentPadding, |
| 23 | + adjustedInsetCompensation | 24 | + adjustedInsetCompensation, |
| 25 | + adjustedStartInsetCompensation | ||
| 24 | }); | 26 | }); |
| 25 | (0, _useExtraContentPadding.useExtraContentPadding)({ | 27 | (0, _useExtraContentPadding.useExtraContentPadding)({ |
| 26 | scrollViewRef, | 28 | scrollViewRef, |
| ... | @@ -28,17 +30,23 @@ index db8cfb1d289f91563f13c4dd842c783c99facc32..940e1dd80c6c9dfc42eab916445be414 | ... | @@ -28,17 +30,23 @@ index db8cfb1d289f91563f13c4dd842c783c99facc32..940e1dd80c6c9dfc42eab916445be414 |
| 28 | keyboardPadding: padding, | 30 | keyboardPadding: padding, |
| 29 | blankSpace, | 31 | blankSpace, |
| 30 | + adjustedInsetCompensation, | 32 | + adjustedInsetCompensation, |
| 33 | + adjustedStartInsetCompensation, | ||
| 31 | scroll, | 34 | scroll, |
| 32 | layout, | 35 | layout, |
| 33 | size, | 36 | size, |
| 34 | @@ -82,10 +86,21 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ | 37 | @@ -82,10 +89,26 @@ const KeyboardChatScrollView = /*#__PURE__*/(0, _react.forwardRef)(({ |
| 35 | // a bug for you, please open an issue. | 38 | // a bug for you, please open an issue. |
| 36 | const totalPadding = (0, _reactNativeReanimated.useDerivedValue)(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); | 39 | const totalPadding = (0, _reactNativeReanimated.useDerivedValue)(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); |
| 37 | 40 | ||
| 41 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 42 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 43 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 44 | + const reportedPadding = (0, _reactNativeReanimated.useDerivedValue)(() => _reactNative.Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); | ||
| 45 | + | ||
| 38 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | 46 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) |
| 39 | + // to the consumer - a virtualized list needs it in its own scroll math or | 47 | + // to the consumer - a virtualized list needs it in its own scroll math or |
| 40 | + // its end/maintain targets point at the under-the-keyboard resting offset. | 48 | + // its end/maintain targets point at the under-the-keyboard resting offset. |
| 41 | + (0, _reactNativeReanimated.useAnimatedReaction)(() => totalPadding.value, (current, previous) => { | 49 | + (0, _reactNativeReanimated.useAnimatedReaction)(() => reportedPadding.value, (current, previous) => { |
| 42 | + if (onContentInsetChange && current !== previous) { | 50 | + if (onContentInsetChange && current !== previous) { |
| 43 | + (0, _reactNativeReanimated.runOnJS)(onContentInsetChange)({ | 51 | + (0, _reactNativeReanimated.runOnJS)(onContentInsetChange)({ |
| 44 | + bottom: current | 52 | + bottom: current |
| ... | @@ -54,21 +62,57 @@ index db8cfb1d289f91563f13c4dd842c783c99facc32..940e1dd80c6c9dfc42eab916445be414 | ... | @@ -54,21 +62,57 @@ index db8cfb1d289f91563f13c4dd842c783c99facc32..940e1dd80c6c9dfc42eab916445be414 |
| 54 | const onLayout = (0, _react.useCallback)(e => { | 62 | const onLayout = (0, _react.useCallback)(e => { |
| 55 | onLayoutInternal(e); | 63 | onLayoutInternal(e); |
| 56 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); | 64 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); |
| 65 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 66 | index 4d33b3fe426070cf312fc493ce902b2c24d853ea..e619708b95865afd8b43dd8f0a6523fcf67b1f53 100644 | ||
| 67 | --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 68 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 69 | @@ -206,7 +206,7 @@ const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentHeight, | ||
| 70 | * ``` | ||
| 71 | */ | ||
| 72 | exports.clampedScrollTarget = clampedScrollTarget; | ||
| 73 | -const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { | ||
| 74 | +const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { | ||
| 75 | "worklet"; | ||
| 76 | |||
| 77 | const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; | ||
| 78 | @@ -214,8 +214,9 @@ const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, | ||
| 79 | const maxScroll = Math.max(contentHeight - layoutHeight, 0); | ||
| 80 | return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); | ||
| 81 | } | ||
| 82 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 83 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 84 | + const minScroll = -startInsetCompensation; | ||
| 85 | + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); | ||
| 86 | + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); | ||
| 87 | }; | ||
| 88 | exports.computeIOSContentOffset = computeIOSContentOffset; | ||
| 89 | //# sourceMappingURL=helpers.js.map | ||
| 57 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | 90 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js |
| 58 | index 2073da84b8b2be291aa3181700c2b18a75d0fc56..f43f2efdc4f0eda0460720839544dc6e7f4a54e0 100644 | 91 | index 2073da84b8b2be291aa3181700c2b18a75d0fc56..7ae9bb220ce3cc23c788a79677afdc7d9547a27c 100644 |
| 59 | --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | 92 | --- a/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js |
| 60 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | 93 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js |
| 61 | @@ -32,7 +32,8 @@ function useChatKeyboard(scrollViewRef, options) { | 94 | @@ -32,7 +32,9 @@ function useChatKeyboard(scrollViewRef, options) { |
| 62 | freeze, | 95 | freeze, |
| 63 | offset, | 96 | offset, |
| 64 | blankSpace, | 97 | blankSpace, |
| 65 | - extraContentPadding | 98 | - extraContentPadding |
| 66 | + extraContentPadding, | 99 | + extraContentPadding, |
| 67 | + adjustedInsetCompensation | 100 | + adjustedInsetCompensation, |
| 101 | + adjustedStartInsetCompensation | ||
| 68 | } = options; | 102 | } = options; |
| 69 | const padding = (0, _reactNativeReanimated.useSharedValue)(0); | 103 | const padding = (0, _reactNativeReanimated.useSharedValue)(0); |
| 70 | const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0); | 104 | const currentHeight = (0, _reactNativeReanimated.useSharedValue)(0); |
| 71 | @@ -66,7 +67,7 @@ function useChatKeyboard(scrollViewRef, options) { | 105 | @@ -58,6 +60,9 @@ function useChatKeyboard(scrollViewRef, options) { |
| 106 | targetKeyboardHeight.value = e.height; | ||
| 107 | } | ||
| 108 | const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 109 | + if (e.duration === 0) { | ||
| 110 | + currentHeight.value = effective; | ||
| 111 | + } | ||
| 112 | const atEnd = (0, _helpers.isScrollAtEnd)(scroll.value, layout.value.height, size.value.height, inverted); | ||
| 113 | |||
| 114 | // Scale minimum padding absorption by how much of it is visible. | ||
| 115 | @@ -66,7 +71,7 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 72 | const visiblePadding = visibleFraction * blankSpace.value; | 116 | const visiblePadding = visibleFraction * blankSpace.value; |
| 73 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); | 117 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); |
| 74 | const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); | 118 | const scrollEffective = (0, _helpers.getScrollEffective)(effective, minimumPaddingAbsorbed); |
| ... | @@ -77,28 +121,81 @@ index 2073da84b8b2be291aa3181700c2b18a75d0fc56..f43f2efdc4f0eda0460720839544dc6e | ... | @@ -77,28 +121,81 @@ index 2073da84b8b2be291aa3181700c2b18a75d0fc56..f43f2efdc4f0eda0460720839544dc6e |
| 77 | 121 | ||
| 78 | // persistent mode: when keyboard shrinks, clamp to valid range | 122 | // persistent mode: when keyboard shrinks, clamp to valid range |
| 79 | if (keyboardLiftBehavior === "persistent" && effective < padding.value) { | 123 | if (keyboardLiftBehavior === "persistent" && effective < padding.value) { |
| 80 | @@ -134,7 +135,7 @@ function useChatKeyboard(scrollViewRef, options) { | 124 | @@ -76,8 +81,8 @@ function useChatKeyboard(scrollViewRef, options) { |
| 125 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 126 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 127 | } else { | ||
| 128 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 129 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 130 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 131 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 132 | } | ||
| 133 | return; | ||
| 134 | } | ||
| 135 | @@ -91,8 +96,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 136 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 137 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 138 | } else { | ||
| 139 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 140 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 141 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 142 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 143 | } | ||
| 144 | return; | ||
| 145 | } | ||
| 146 | @@ -118,12 +123,23 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 147 | contentOffsetY.value = scroll.value; | ||
| 148 | return; | ||
| 149 | } | ||
| 150 | - contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); | ||
| 151 | + contentOffsetY.value = (0, _helpers.computeIOSContentOffset)(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); | ||
| 152 | + }, | ||
| 153 | + onMove: e => { | ||
| 154 | + "worklet"; | ||
| 155 | + | ||
| 156 | + if (freeze.value) { | ||
| 157 | + return; | ||
| 158 | + } | ||
| 159 | + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 160 | }, | ||
| 161 | - onMove: () => { | ||
| 162 | + onInteractive: e => { | ||
| 163 | "worklet"; | ||
| 164 | |||
| 165 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 166 | + if (freeze.value) { | ||
| 167 | + return; | ||
| 168 | + } | ||
| 169 | + currentHeight.value = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | ||
| 170 | }, | ||
| 171 | onEnd: e => { | ||
| 172 | "worklet"; | ||
| 173 | @@ -132,9 +148,10 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 174 | return; | ||
| 175 | } | ||
| 81 | const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); | 176 | const effective = (0, _helpers.getEffectiveHeight)(e.height, targetKeyboardHeight.value, offset); |
| 177 | + currentHeight.value = effective; | ||
| 82 | padding.value = effective; | 178 | padding.value = effective; |
| 83 | } | 179 | } |
| 84 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); | 180 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); |
| 85 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation]); | 181 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); |
| 86 | return { | 182 | return { |
| 87 | padding, | 183 | padding, |
| 88 | currentHeight, | 184 | currentHeight, |
| 89 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | 185 | diff --git a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js |
| 90 | index 0d50bdfeb7bbc5c14a31ed344f8a690e4fd340b3..22ca193257ab0068f732c088b09145dabf39a05e 100644 | 186 | index 0d50bdfeb7bbc5c14a31ed344f8a690e4fd340b3..e0b73653fd1dd23f0f2e2c85835a3d107e3f666d 100644 |
| 91 | --- a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | 187 | --- a/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js |
| 92 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js | 188 | +++ b/lib/commonjs/components/KeyboardChatScrollView/useExtraContentPadding/index.js |
| 93 | @@ -29,6 +29,7 @@ function useExtraContentPadding(options) { | 189 | @@ -29,6 +29,8 @@ function useExtraContentPadding(options) { |
| 94 | extraContentPadding, | 190 | extraContentPadding, |
| 95 | keyboardPadding, | 191 | keyboardPadding, |
| 96 | blankSpace, | 192 | blankSpace, |
| 97 | + adjustedInsetCompensation, | 193 | + adjustedInsetCompensation, |
| 194 | + adjustedStartInsetCompensation, | ||
| 98 | scroll, | 195 | scroll, |
| 99 | layout, | 196 | layout, |
| 100 | size, | 197 | size, |
| 101 | @@ -68,8 +69,8 @@ function useExtraContentPadding(options) { | 198 | @@ -68,8 +70,8 @@ function useExtraContentPadding(options) { |
| 102 | } | 199 | } |
| 103 | 200 | ||
| 104 | // Compute effective delta considering blankSpace floor | 201 | // Compute effective delta considering blankSpace floor |
| ... | @@ -109,33 +206,41 @@ index 0d50bdfeb7bbc5c14a31ed344f8a690e4fd340b3..22ca193257ab0068f732c088b09145da | ... | @@ -109,33 +206,41 @@ index 0d50bdfeb7bbc5c14a31ed344f8a690e4fd340b3..22ca193257ab0068f732c088b09145da |
| 109 | const effectiveDelta = currentTotal - previousTotal; | 206 | const effectiveDelta = currentTotal - previousTotal; |
| 110 | if (effectiveDelta === 0) { | 207 | if (effectiveDelta === 0) { |
| 111 | // blankSpace absorbed the change | 208 | // blankSpace absorbed the change |
| 112 | @@ -92,6 +93,6 @@ function useExtraContentPadding(options) { | 209 | @@ -88,10 +90,11 @@ function useExtraContentPadding(options) { |
| 113 | const target = Math.min(scroll.value + effectiveDelta, maxScroll); | 210 | const target = Math.max(scroll.value - effectiveDelta, -currentTotal); |
| 211 | scrollToTarget(target); | ||
| 212 | } else { | ||
| 213 | - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); | ||
| 214 | - const target = Math.min(scroll.value + effectiveDelta, maxScroll); | ||
| 215 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 216 | + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); | ||
| 217 | + const target = Math.max(minScroll, Math.min(scroll.value + effectiveDelta, maxScroll)); | ||
| 114 | scrollToTarget(target); | 218 | scrollToTarget(target); |
| 115 | } | 219 | } |
| 116 | - }, [inverted, keyboardLiftBehavior]); | 220 | - }, [inverted, keyboardLiftBehavior]); |
| 117 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation]); | 221 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); |
| 118 | } | 222 | } |
| 119 | //# sourceMappingURL=index.js.map | 223 | //# sourceMappingURL=index.js.map |
| 120 | \ No newline at end of file | ||
| 121 | diff --git a/lib/module/components/KeyboardChatScrollView/index.js b/lib/module/components/KeyboardChatScrollView/index.js | 224 | diff --git a/lib/module/components/KeyboardChatScrollView/index.js b/lib/module/components/KeyboardChatScrollView/index.js |
| 122 | index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..ac79433fdf0b36f89525b9840447a116da63d58c 100644 | 225 | index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..76b858a6590ad0592cc663864ea822db55eaeceb 100644 |
| 123 | --- a/lib/module/components/KeyboardChatScrollView/index.js | 226 | --- a/lib/module/components/KeyboardChatScrollView/index.js |
| 124 | +++ b/lib/module/components/KeyboardChatScrollView/index.js | 227 | +++ b/lib/module/components/KeyboardChatScrollView/index.js |
| 125 | @@ -1,7 +1,7 @@ | 228 | @@ -1,7 +1,7 @@ |
| 126 | function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } | 229 | function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } |
| 127 | import React, { forwardRef, useCallback, useMemo } from "react"; | 230 | import React, { forwardRef, useCallback, useMemo } from "react"; |
| 128 | import { StyleSheet } from "react-native"; | 231 | -import { StyleSheet } from "react-native"; |
| 129 | -import { makeMutable, useAnimatedRef, useAnimatedStyle, useDerivedValue } from "react-native-reanimated"; | 232 | -import { makeMutable, useAnimatedRef, useAnimatedStyle, useDerivedValue } from "react-native-reanimated"; |
| 233 | +import { Platform, StyleSheet } from "react-native"; | ||
| 130 | +import { makeMutable, runOnJS, useAnimatedReaction, useAnimatedRef, useAnimatedStyle, useDerivedValue } from "react-native-reanimated"; | 234 | +import { makeMutable, runOnJS, useAnimatedReaction, useAnimatedRef, useAnimatedStyle, useDerivedValue } from "react-native-reanimated"; |
| 131 | import Reanimated from "react-native-reanimated"; | 235 | import Reanimated from "react-native-reanimated"; |
| 132 | import useCombinedRef from "../hooks/useCombinedRef"; | 236 | import useCombinedRef from "../hooks/useCombinedRef"; |
| 133 | import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding"; | 237 | import ScrollViewWithBottomPadding from "../ScrollViewWithBottomPadding"; |
| 134 | @@ -19,9 +19,11 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | 238 | @@ -19,9 +19,12 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ |
| 135 | offset = 0, | 239 | offset = 0, |
| 136 | extraContentPadding = ZERO_CONTENT_PADDING, | 240 | extraContentPadding = ZERO_CONTENT_PADDING, |
| 137 | blankSpace = ZERO_BLANK_SPACE, | 241 | blankSpace = ZERO_BLANK_SPACE, |
| 138 | + adjustedInsetCompensation = 0, | 242 | + adjustedInsetCompensation = 0, |
| 243 | + adjustedStartInsetCompensation = 0, | ||
| 139 | applyWorkaroundForContentInsetHitTestBug = false, | 244 | applyWorkaroundForContentInsetHitTestBug = false, |
| 140 | onLayout: onLayoutProp, | 245 | onLayout: onLayoutProp, |
| 141 | onContentSizeChange: onContentSizeChangeProp, | 246 | onContentSizeChange: onContentSizeChangeProp, |
| ... | @@ -143,13 +248,14 @@ index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..ac79433fdf0b36f89525b9840447a116 | ... | @@ -143,13 +248,14 @@ index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..ac79433fdf0b36f89525b9840447a116 |
| 143 | onEndVisible, | 248 | onEndVisible, |
| 144 | ...rest | 249 | ...rest |
| 145 | }, ref) => { | 250 | }, ref) => { |
| 146 | @@ -43,13 +45,15 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | 251 | @@ -43,13 +46,17 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ |
| 147 | freeze: freezeSV, | 252 | freeze: freezeSV, |
| 148 | offset, | 253 | offset, |
| 149 | blankSpace, | 254 | blankSpace, |
| 150 | - extraContentPadding | 255 | - extraContentPadding |
| 151 | + extraContentPadding, | 256 | + extraContentPadding, |
| 152 | + adjustedInsetCompensation | 257 | + adjustedInsetCompensation, |
| 258 | + adjustedStartInsetCompensation | ||
| 153 | }); | 259 | }); |
| 154 | useExtraContentPadding({ | 260 | useExtraContentPadding({ |
| 155 | scrollViewRef, | 261 | scrollViewRef, |
| ... | @@ -157,17 +263,23 @@ index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..ac79433fdf0b36f89525b9840447a116 | ... | @@ -157,17 +263,23 @@ index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..ac79433fdf0b36f89525b9840447a116 |
| 157 | keyboardPadding: padding, | 263 | keyboardPadding: padding, |
| 158 | blankSpace, | 264 | blankSpace, |
| 159 | + adjustedInsetCompensation, | 265 | + adjustedInsetCompensation, |
| 266 | + adjustedStartInsetCompensation, | ||
| 160 | scroll, | 267 | scroll, |
| 161 | layout, | 268 | layout, |
| 162 | size, | 269 | size, |
| 163 | @@ -75,10 +79,21 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ | 270 | @@ -75,10 +82,26 @@ const KeyboardChatScrollView = /*#__PURE__*/forwardRef(({ |
| 164 | // a bug for you, please open an issue. | 271 | // a bug for you, please open an issue. |
| 165 | const totalPadding = useDerivedValue(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); | 272 | const totalPadding = useDerivedValue(() => Math.min(layout.value.height, Math.max(blankSpace.value, padding.value + extraContentPadding.value))); |
| 166 | 273 | ||
| 274 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 275 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 276 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 277 | + const reportedPadding = useDerivedValue(() => Platform.OS === "ios" ? Math.min(layout.value.height, Math.max(blankSpace.value, currentHeight.value + extraContentPadding.value)) : totalPadding.value); | ||
| 278 | + | ||
| 167 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | 279 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) |
| 168 | + // to the consumer - a virtualized list needs it in its own scroll math or | 280 | + // to the consumer - a virtualized list needs it in its own scroll math or |
| 169 | + // its end/maintain targets point at the under-the-keyboard resting offset. | 281 | + // its end/maintain targets point at the under-the-keyboard resting offset. |
| 170 | + useAnimatedReaction(() => totalPadding.value, (current, previous) => { | 282 | + useAnimatedReaction(() => reportedPadding.value, (current, previous) => { |
| 171 | + if (onContentInsetChange && current !== previous) { | 283 | + if (onContentInsetChange && current !== previous) { |
| 172 | + runOnJS(onContentInsetChange)({ | 284 | + runOnJS(onContentInsetChange)({ |
| 173 | + bottom: current | 285 | + bottom: current |
| ... | @@ -183,21 +295,56 @@ index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..ac79433fdf0b36f89525b9840447a116 | ... | @@ -183,21 +295,56 @@ index 612dd8bd9bd6cc3e30a5acac937ea3383eb1b630..ac79433fdf0b36f89525b9840447a116 |
| 183 | const onLayout = useCallback(e => { | 295 | const onLayout = useCallback(e => { |
| 184 | onLayoutInternal(e); | 296 | onLayoutInternal(e); |
| 185 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); | 297 | onLayoutProp === null || onLayoutProp === void 0 || onLayoutProp(e); |
| 298 | diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 299 | index 295e2219b560519b8d033968b5105d1da3e77315..ff7ee3fe0d880c2287a7b70d5ecf952e99357adf 100644 | ||
| 300 | --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 301 | +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/helpers.js | ||
| 302 | @@ -193,7 +193,7 @@ export const clampedScrollTarget = (offsetBeforeScroll, keyboardHeight, contentH | ||
| 303 | * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 | ||
| 304 | * ``` | ||
| 305 | */ | ||
| 306 | -export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll) => { | ||
| 307 | +export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentHeight, layoutHeight, inverted, totalPaddingForMaxScroll, startInsetCompensation = 0) => { | ||
| 308 | "worklet"; | ||
| 309 | |||
| 310 | const paddingForMax = totalPaddingForMaxScroll !== undefined ? totalPaddingForMaxScroll : keyboardHeight; | ||
| 311 | @@ -201,7 +201,8 @@ export const computeIOSContentOffset = (relativeScroll, keyboardHeight, contentH | ||
| 312 | const maxScroll = Math.max(contentHeight - layoutHeight, 0); | ||
| 313 | return Math.max(Math.min(relativeScroll - keyboardHeight, maxScroll), -paddingForMax); | ||
| 314 | } | ||
| 315 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 316 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 317 | + const minScroll = -startInsetCompensation; | ||
| 318 | + const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, minScroll); | ||
| 319 | + return Math.min(Math.max(keyboardHeight + relativeScroll, minScroll), maxScroll); | ||
| 320 | }; | ||
| 321 | //# sourceMappingURL=helpers.js.map | ||
| 186 | diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | 322 | diff --git a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js |
| 187 | index 52943c3a7d6a68fe2094dc1d112c07e6b9d890e4..c8685c18a53ad078c24fc3e3b6572669b2b63397 100644 | 323 | index 52943c3a7d6a68fe2094dc1d112c07e6b9d890e4..cc4bfae859191aeb848179028b7217098cba2340 100644 |
| 188 | --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | 324 | --- a/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js |
| 189 | +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js | 325 | +++ b/lib/module/components/KeyboardChatScrollView/useChatKeyboard/index.ios.js |
| 190 | @@ -25,7 +25,8 @@ function useChatKeyboard(scrollViewRef, options) { | 326 | @@ -25,7 +25,9 @@ function useChatKeyboard(scrollViewRef, options) { |
| 191 | freeze, | 327 | freeze, |
| 192 | offset, | 328 | offset, |
| 193 | blankSpace, | 329 | blankSpace, |
| 194 | - extraContentPadding | 330 | - extraContentPadding |
| 195 | + extraContentPadding, | 331 | + extraContentPadding, |
| 196 | + adjustedInsetCompensation | 332 | + adjustedInsetCompensation, |
| 333 | + adjustedStartInsetCompensation | ||
| 197 | } = options; | 334 | } = options; |
| 198 | const padding = useSharedValue(0); | 335 | const padding = useSharedValue(0); |
| 199 | const currentHeight = useSharedValue(0); | 336 | const currentHeight = useSharedValue(0); |
| 200 | @@ -59,7 +60,7 @@ function useChatKeyboard(scrollViewRef, options) { | 337 | @@ -51,6 +53,9 @@ function useChatKeyboard(scrollViewRef, options) { |
| 338 | targetKeyboardHeight.value = e.height; | ||
| 339 | } | ||
| 340 | const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 341 | + if (e.duration === 0) { | ||
| 342 | + currentHeight.value = effective; | ||
| 343 | + } | ||
| 344 | const atEnd = isScrollAtEnd(scroll.value, layout.value.height, size.value.height, inverted); | ||
| 345 | |||
| 346 | // Scale minimum padding absorption by how much of it is visible. | ||
| 347 | @@ -59,7 +64,7 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 201 | const visiblePadding = visibleFraction * blankSpace.value; | 348 | const visiblePadding = visibleFraction * blankSpace.value; |
| 202 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); | 349 | const minimumPaddingAbsorbed = Math.max(0, visiblePadding - extraContentPadding.value); |
| 203 | const scrollEffective = getScrollEffective(effective, minimumPaddingAbsorbed); | 350 | const scrollEffective = getScrollEffective(effective, minimumPaddingAbsorbed); |
| ... | @@ -206,28 +353,81 @@ index 52943c3a7d6a68fe2094dc1d112c07e6b9d890e4..c8685c18a53ad078c24fc3e3b6572669 | ... | @@ -206,28 +353,81 @@ index 52943c3a7d6a68fe2094dc1d112c07e6b9d890e4..c8685c18a53ad078c24fc3e3b6572669 |
| 206 | 353 | ||
| 207 | // persistent mode: when keyboard shrinks, clamp to valid range | 354 | // persistent mode: when keyboard shrinks, clamp to valid range |
| 208 | if (keyboardLiftBehavior === "persistent" && effective < padding.value) { | 355 | if (keyboardLiftBehavior === "persistent" && effective < padding.value) { |
| 209 | @@ -127,7 +128,7 @@ function useChatKeyboard(scrollViewRef, options) { | 356 | @@ -69,8 +74,8 @@ function useChatKeyboard(scrollViewRef, options) { |
| 357 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 358 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 359 | } else { | ||
| 360 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 361 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 362 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 363 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 364 | } | ||
| 365 | return; | ||
| 366 | } | ||
| 367 | @@ -84,8 +89,8 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 368 | const maxScroll = Math.max(size.value.height - layout.value.height, 0); | ||
| 369 | contentOffsetY.value = Math.max(-actualTotalPadding, Math.min(scroll.value, maxScroll)); | ||
| 370 | } else { | ||
| 371 | - const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, 0); | ||
| 372 | - contentOffsetY.value = Math.max(0, Math.min(scroll.value, maxScroll)); | ||
| 373 | + const maxScroll = Math.max(size.value.height - layout.value.height + actualTotalPadding, -adjustedStartInsetCompensation); | ||
| 374 | + contentOffsetY.value = Math.max(-adjustedStartInsetCompensation, Math.min(scroll.value, maxScroll)); | ||
| 375 | } | ||
| 376 | return; | ||
| 377 | } | ||
| 378 | @@ -111,12 +116,23 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 379 | contentOffsetY.value = scroll.value; | ||
| 380 | return; | ||
| 381 | } | ||
| 382 | - contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding); | ||
| 383 | + contentOffsetY.value = computeIOSContentOffset(relativeScroll, scrollEffective, size.value.height, layout.value.height, inverted, actualTotalPadding, adjustedStartInsetCompensation); | ||
| 384 | + }, | ||
| 385 | + onMove: e => { | ||
| 386 | + "worklet"; | ||
| 387 | + | ||
| 388 | + if (freeze.value) { | ||
| 389 | + return; | ||
| 390 | + } | ||
| 391 | + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 392 | }, | ||
| 393 | - onMove: () => { | ||
| 394 | + onInteractive: e => { | ||
| 395 | "worklet"; | ||
| 396 | |||
| 397 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 398 | + if (freeze.value) { | ||
| 399 | + return; | ||
| 400 | + } | ||
| 401 | + currentHeight.value = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | ||
| 402 | }, | ||
| 403 | onEnd: e => { | ||
| 404 | "worklet"; | ||
| 405 | @@ -125,9 +141,10 @@ function useChatKeyboard(scrollViewRef, options) { | ||
| 406 | return; | ||
| 407 | } | ||
| 210 | const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); | 408 | const effective = getEffectiveHeight(e.height, targetKeyboardHeight.value, offset); |
| 409 | + currentHeight.value = effective; | ||
| 211 | padding.value = effective; | 410 | padding.value = effective; |
| 212 | } | 411 | } |
| 213 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); | 412 | - }, [inverted, keyboardLiftBehavior, offset, extraContentPadding]); |
| 214 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation]); | 413 | + }, [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation, adjustedStartInsetCompensation]); |
| 215 | return { | 414 | return { |
| 216 | padding, | 415 | padding, |
| 217 | currentHeight, | 416 | currentHeight, |
| 218 | diff --git a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | 417 | diff --git a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js |
| 219 | index 1afa50987a8d2a5fe3f36b20945efe804d48a873..e2966d89d4329a7dc1233ff060cab6f365f745d6 100644 | 418 | index 1afa50987a8d2a5fe3f36b20945efe804d48a873..7b05d0721c50e118cf78c07913b580a31293db77 100644 |
| 220 | --- a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | 419 | --- a/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js |
| 221 | +++ b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js | 420 | +++ b/lib/module/components/KeyboardChatScrollView/useExtraContentPadding/index.js |
| 222 | @@ -23,6 +23,7 @@ function useExtraContentPadding(options) { | 421 | @@ -23,6 +23,8 @@ function useExtraContentPadding(options) { |
| 223 | extraContentPadding, | 422 | extraContentPadding, |
| 224 | keyboardPadding, | 423 | keyboardPadding, |
| 225 | blankSpace, | 424 | blankSpace, |
| 226 | + adjustedInsetCompensation, | 425 | + adjustedInsetCompensation, |
| 426 | + adjustedStartInsetCompensation, | ||
| 227 | scroll, | 427 | scroll, |
| 228 | layout, | 428 | layout, |
| 229 | size, | 429 | size, |
| 230 | @@ -62,8 +63,8 @@ function useExtraContentPadding(options) { | 430 | @@ -62,8 +64,8 @@ function useExtraContentPadding(options) { |
| 231 | } | 431 | } |
| 232 | 432 | ||
| 233 | // Compute effective delta considering blankSpace floor | 433 | // Compute effective delta considering blankSpace floor |
| ... | @@ -238,61 +438,85 @@ index 1afa50987a8d2a5fe3f36b20945efe804d48a873..e2966d89d4329a7dc1233ff060cab6f3 | ... | @@ -238,61 +438,85 @@ index 1afa50987a8d2a5fe3f36b20945efe804d48a873..e2966d89d4329a7dc1233ff060cab6f3 |
| 238 | const effectiveDelta = currentTotal - previousTotal; | 438 | const effectiveDelta = currentTotal - previousTotal; |
| 239 | if (effectiveDelta === 0) { | 439 | if (effectiveDelta === 0) { |
| 240 | // blankSpace absorbed the change | 440 | // blankSpace absorbed the change |
| 241 | @@ -86,7 +87,7 @@ function useExtraContentPadding(options) { | 441 | @@ -82,11 +84,12 @@ function useExtraContentPadding(options) { |
| 242 | const target = Math.min(scroll.value + effectiveDelta, maxScroll); | 442 | const target = Math.max(scroll.value - effectiveDelta, -currentTotal); |
| 443 | scrollToTarget(target); | ||
| 444 | } else { | ||
| 445 | - const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, 0); | ||
| 446 | - const target = Math.min(scroll.value + effectiveDelta, maxScroll); | ||
| 447 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 448 | + const maxScroll = Math.max(size.value.height - layout.value.height + currentTotal, minScroll); | ||
| 449 | + const target = Math.max(minScroll, Math.min(scroll.value + effectiveDelta, maxScroll)); | ||
| 243 | scrollToTarget(target); | 450 | scrollToTarget(target); |
| 244 | } | 451 | } |
| 245 | - }, [inverted, keyboardLiftBehavior]); | 452 | - }, [inverted, keyboardLiftBehavior]); |
| 246 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation]); | 453 | + }, [inverted, keyboardLiftBehavior, adjustedInsetCompensation, adjustedStartInsetCompensation]); |
| 247 | } | 454 | } |
| 248 | export { useExtraContentPadding }; | 455 | export { useExtraContentPadding }; |
| 249 | //# sourceMappingURL=index.js.map | 456 | //# sourceMappingURL=index.js.map |
| 250 | \ No newline at end of file | ||
| 251 | diff --git a/lib/typescript/components/KeyboardChatScrollView/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/types.d.ts | 457 | diff --git a/lib/typescript/components/KeyboardChatScrollView/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/types.d.ts |
| 252 | index a036b431f03efb9d5379527c17db6fce62bfee09..6ca6fdf60fc9fae1e28a86cf24e21beed99b3a76 100644 | 458 | index a036b431f03efb9d5379527c17db6fce62bfee09..5e56bf7edec9161698a745e8e6292bfccd68d3db 100644 |
| 253 | --- a/lib/typescript/components/KeyboardChatScrollView/types.d.ts | 459 | --- a/lib/typescript/components/KeyboardChatScrollView/types.d.ts |
| 254 | +++ b/lib/typescript/components/KeyboardChatScrollView/types.d.ts | 460 | +++ b/lib/typescript/components/KeyboardChatScrollView/types.d.ts |
| 255 | @@ -86,6 +86,8 @@ export type KeyboardChatScrollViewProps = { | 461 | @@ -86,6 +86,10 @@ export type KeyboardChatScrollViewProps = { |
| 256 | * Default is `undefined` (equivalent to `0` — no minimum floor). | 462 | * Default is `undefined` (equivalent to `0` — no minimum floor). |
| 257 | */ | 463 | */ |
| 258 | blankSpace?: SharedValue<number>; | 464 | blankSpace?: SharedValue<number>; |
| 259 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | 465 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ |
| 260 | + adjustedInsetCompensation?: number; | 466 | + adjustedInsetCompensation?: number; |
| 467 | + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ | ||
| 468 | + adjustedStartInsetCompensation?: number; | ||
| 261 | /** | 469 | /** |
| 262 | * Fires whenever the effective content inset changes — the static `contentInset` | 470 | * Fires whenever the effective content inset changes — the static `contentInset` |
| 263 | * prop combined with the dynamic keyboard-driven padding. | 471 | * prop combined with the dynamic keyboard-driven padding. |
| 472 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 473 | index 2b51cc4737b4a05b5396da31e67e54177f43c498..3acec1fbddee6227915590a6c8d449757cb4d7b2 100644 | ||
| 474 | --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 475 | +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/helpers.d.ts | ||
| 476 | @@ -129,4 +129,4 @@ export declare const clampedScrollTarget: (offsetBeforeScroll: number, keyboardH | ||
| 477 | * computeIOSContentOffset(100, 300, 1000, 800, false); // 400 | ||
| 478 | * ``` | ||
| 479 | */ | ||
| 480 | -export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number) => number; | ||
| 481 | +export declare const computeIOSContentOffset: (relativeScroll: number, keyboardHeight: number, contentHeight: number, layoutHeight: number, inverted: boolean, totalPaddingForMaxScroll?: number, startInsetCompensation?: number) => number; | ||
| 264 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | 482 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts |
| 265 | index aff9b5a8dbc2464546396437eaf6c5ae955b9f29..67edbe1a1eac27b5a571611979753ebf3134bc8b 100644 | 483 | index aff9b5a8dbc2464546396437eaf6c5ae955b9f29..fdd83953271123be6caefaa786c469f0d92cce2b 100644 |
| 266 | --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | 484 | --- a/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts |
| 267 | +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts | 485 | +++ b/lib/typescript/components/KeyboardChatScrollView/useChatKeyboard/types.d.ts |
| 268 | @@ -9,6 +9,8 @@ type UseChatKeyboardOptions = { | 486 | @@ -9,6 +9,10 @@ type UseChatKeyboardOptions = { |
| 269 | blankSpace: SharedValue<number>; | 487 | blankSpace: SharedValue<number>; |
| 270 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ | 488 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ |
| 271 | extraContentPadding: SharedValue<number>; | 489 | extraContentPadding: SharedValue<number>; |
| 272 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ | 490 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ |
| 273 | + adjustedInsetCompensation: number; | 491 | + adjustedInsetCompensation: number; |
| 492 | + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ | ||
| 493 | + adjustedStartInsetCompensation: number; | ||
| 274 | }; | 494 | }; |
| 275 | type UseChatKeyboardReturn = { | 495 | type UseChatKeyboardReturn = { |
| 276 | /** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */ | 496 | /** Extra scrollable space (= keyboard height). Used as contentInset on iOS, contentInsetBottom/contentInsetTop on Android. */ |
| 277 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | 497 | diff --git a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts |
| 278 | index ec73f70544a062fbecfc1d8be92d839d3e0bea6f..6fe16cd0b1200a2f4d4d8eeb9b555747186dbfe0 100644 | 498 | index ec73f70544a062fbecfc1d8be92d839d3e0bea6f..45b387446587fd3e611fa02a01534bb5d9fa6bbd 100644 |
| 279 | --- a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | 499 | --- a/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts |
| 280 | +++ b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts | 500 | +++ b/lib/typescript/components/KeyboardChatScrollView/useExtraContentPadding/index.d.ts |
| 281 | @@ -8,6 +8,8 @@ type UseExtraContentPaddingOptions = { | 501 | @@ -8,6 +8,10 @@ type UseExtraContentPaddingOptions = { |
| 282 | keyboardPadding: SharedValue<number>; | 502 | keyboardPadding: SharedValue<number>; |
| 283 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ | 503 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ |
| 284 | blankSpace: SharedValue<number>; | 504 | blankSpace: SharedValue<number>; |
| 285 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ | 505 | + /** Safe-area extra beyond raw contentInset. Offset math only. */ |
| 286 | + adjustedInsetCompensation: number; | 506 | + adjustedInsetCompensation: number; |
| 507 | + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ | ||
| 508 | + adjustedStartInsetCompensation: number; | ||
| 287 | /** Current vertical scroll offset. */ | 509 | /** Current vertical scroll offset. */ |
| 288 | scroll: SharedValue<number>; | 510 | scroll: SharedValue<number>; |
| 289 | /** Visible viewport dimensions. */ | 511 | /** Visible viewport dimensions. */ |
| 290 | diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx | 512 | diff --git a/src/components/KeyboardChatScrollView/index.tsx b/src/components/KeyboardChatScrollView/index.tsx |
| 291 | index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35dca6056f3 100644 | 513 | index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..277a1ecada428ffbae517d6edc720d029c8ecc10 100644 |
| 292 | --- a/src/components/KeyboardChatScrollView/index.tsx | 514 | --- a/src/components/KeyboardChatScrollView/index.tsx |
| 293 | +++ b/src/components/KeyboardChatScrollView/index.tsx | 515 | +++ b/src/components/KeyboardChatScrollView/index.tsx |
| 294 | @@ -2,6 +2,8 @@ import React, { forwardRef, useCallback, useMemo } from "react"; | 516 | @@ -1,7 +1,9 @@ |
| 295 | import { StyleSheet } from "react-native"; | 517 | import React, { forwardRef, useCallback, useMemo } from "react"; |
| 518 | -import { StyleSheet } from "react-native"; | ||
| 519 | +import { Platform, StyleSheet } from "react-native"; | ||
| 296 | import { | 520 | import { |
| 297 | makeMutable, | 521 | makeMutable, |
| 298 | + runOnJS, | 522 | + runOnJS, |
| ... | @@ -300,11 +524,12 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d | ... | @@ -300,11 +524,12 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d |
| 300 | useAnimatedRef, | 524 | useAnimatedRef, |
| 301 | useAnimatedStyle, | 525 | useAnimatedStyle, |
| 302 | useDerivedValue, | 526 | useDerivedValue, |
| 303 | @@ -35,9 +37,11 @@ const KeyboardChatScrollView = forwardRef< | 527 | @@ -35,9 +37,12 @@ const KeyboardChatScrollView = forwardRef< |
| 304 | offset = 0, | 528 | offset = 0, |
| 305 | extraContentPadding = ZERO_CONTENT_PADDING, | 529 | extraContentPadding = ZERO_CONTENT_PADDING, |
| 306 | blankSpace = ZERO_BLANK_SPACE, | 530 | blankSpace = ZERO_BLANK_SPACE, |
| 307 | + adjustedInsetCompensation = 0, | 531 | + adjustedInsetCompensation = 0, |
| 532 | + adjustedStartInsetCompensation = 0, | ||
| 308 | applyWorkaroundForContentInsetHitTestBug = false, | 533 | applyWorkaroundForContentInsetHitTestBug = false, |
| 309 | onLayout: onLayoutProp, | 534 | onLayout: onLayoutProp, |
| 310 | onContentSizeChange: onContentSizeChangeProp, | 535 | onContentSizeChange: onContentSizeChangeProp, |
| ... | @@ -312,23 +537,25 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d | ... | @@ -312,23 +537,25 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d |
| 312 | onEndVisible, | 537 | onEndVisible, |
| 313 | ...rest | 538 | ...rest |
| 314 | }, | 539 | }, |
| 315 | @@ -64,6 +68,7 @@ const KeyboardChatScrollView = forwardRef< | 540 | @@ -64,6 +69,8 @@ const KeyboardChatScrollView = forwardRef< |
| 316 | offset, | 541 | offset, |
| 317 | blankSpace, | 542 | blankSpace, |
| 318 | extraContentPadding, | 543 | extraContentPadding, |
| 319 | + adjustedInsetCompensation, | 544 | + adjustedInsetCompensation, |
| 545 | + adjustedStartInsetCompensation, | ||
| 320 | }); | 546 | }); |
| 321 | 547 | ||
| 322 | useExtraContentPadding({ | 548 | useExtraContentPadding({ |
| 323 | @@ -71,6 +76,7 @@ const KeyboardChatScrollView = forwardRef< | 549 | @@ -71,6 +78,8 @@ const KeyboardChatScrollView = forwardRef< |
| 324 | extraContentPadding, | 550 | extraContentPadding, |
| 325 | keyboardPadding: padding, | 551 | keyboardPadding: padding, |
| 326 | blankSpace, | 552 | blankSpace, |
| 327 | + adjustedInsetCompensation, | 553 | + adjustedInsetCompensation, |
| 554 | + adjustedStartInsetCompensation, | ||
| 328 | scroll, | 555 | scroll, |
| 329 | layout, | 556 | layout, |
| 330 | size, | 557 | size, |
| 331 | @@ -102,13 +108,25 @@ const KeyboardChatScrollView = forwardRef< | 558 | @@ -102,13 +111,40 @@ const KeyboardChatScrollView = forwardRef< |
| 332 | ), | 559 | ), |
| 333 | ); | 560 | ); |
| 334 | 561 | ||
| ... | @@ -337,19 +564,34 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d | ... | @@ -337,19 +564,34 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d |
| 337 | - // scrollIndicatorInsets adjustment at the application layer. | 564 | - // scrollIndicatorInsets adjustment at the application layer. |
| 338 | - const indicatorPadding = useDerivedValue( | 565 | - const indicatorPadding = useDerivedValue( |
| 339 | - () => padding.value + extraContentPadding.value, | 566 | - () => padding.value + extraContentPadding.value, |
| 567 | + // iOS applies the destination contentInset and contentOffset together at | ||
| 568 | + // keyboard-animation start. Keep that native target behavior, but report | ||
| 569 | + // the presentation height so virtualized-list layout follows the keyboard. | ||
| 570 | + const reportedPadding = useDerivedValue(() => | ||
| 571 | + Platform.OS === "ios" | ||
| 572 | + ? Math.min( | ||
| 573 | + layout.value.height, | ||
| 574 | + Math.max( | ||
| 575 | + blankSpace.value, | ||
| 576 | + currentHeight.value + extraContentPadding.value, | ||
| 577 | + ), | ||
| 578 | + ) | ||
| 579 | + : totalPadding.value, | ||
| 580 | ); | ||
| 581 | |||
| 340 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) | 582 | + // Mirror the effective bottom padding (keyboard + composer + blank floor) |
| 341 | + // to the consumer — a virtualized list needs it in its own scroll math or | 583 | + // to the consumer — a virtualized list needs it in its own scroll math or |
| 342 | + // its end/maintain targets point at the under-the-keyboard resting offset. | 584 | + // its end/maintain targets point at the under-the-keyboard resting offset. |
| 343 | + useAnimatedReaction( | 585 | + useAnimatedReaction( |
| 344 | + () => totalPadding.value, | 586 | + () => reportedPadding.value, |
| 345 | + (current, previous) => { | 587 | + (current, previous) => { |
| 346 | + if (onContentInsetChange && current !== previous) { | 588 | + if (onContentInsetChange && current !== previous) { |
| 347 | + runOnJS(onContentInsetChange)({ bottom: current }); | 589 | + runOnJS(onContentInsetChange)({ bottom: current }); |
| 348 | + } | 590 | + } |
| 349 | + }, | 591 | + }, |
| 350 | + [onContentInsetChange], | 592 | + [onContentInsetChange], |
| 351 | ); | 593 | + ); |
| 352 | 594 | + | |
| 353 | + // Scroll indicator inset = keyboard only (excludes extraContentPadding and | 595 | + // Scroll indicator inset = keyboard only (excludes extraContentPadding and |
| 354 | + // blankSpace): with a floating composer the indicator track should run the | 596 | + // blankSpace): with a floating composer the indicator track should run the |
| 355 | + // full height of the scroll view, behind the composer, like iOS Messages. | 597 | + // full height of the scroll view, behind the composer, like iOS Messages. |
| ... | @@ -360,10 +602,10 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d | ... | @@ -360,10 +602,10 @@ index 03f5f74e9aaaabc75db1c01643a655ee4fdfa5f2..d657002ebbce53c47e3a713c0921c35d |
| 360 | (e: LayoutChangeEvent) => { | 602 | (e: LayoutChangeEvent) => { |
| 361 | onLayoutInternal(e); | 603 | onLayoutInternal(e); |
| 362 | diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts | 604 | diff --git a/src/components/KeyboardChatScrollView/types.ts b/src/components/KeyboardChatScrollView/types.ts |
| 363 | index dd222b57bc7a71729524670bad3812f30920bd73..40249a4357991b7a9c69b87214db2ceb6ff4ba7d 100644 | 605 | index dd222b57bc7a71729524670bad3812f30920bd73..7be71b5460eb143ddf460ce5c87b843c66bfb2cd 100644 |
| 364 | --- a/src/components/KeyboardChatScrollView/types.ts | 606 | --- a/src/components/KeyboardChatScrollView/types.ts |
| 365 | +++ b/src/components/KeyboardChatScrollView/types.ts | 607 | +++ b/src/components/KeyboardChatScrollView/types.ts |
| 366 | @@ -90,6 +90,15 @@ export type KeyboardChatScrollViewProps = { | 608 | @@ -90,6 +90,17 @@ export type KeyboardChatScrollViewProps = { |
| 367 | * Default is `undefined` (equivalent to `0` — no minimum floor). | 609 | * Default is `undefined` (equivalent to `0` — no minimum floor). |
| 368 | */ | 610 | */ |
| 369 | blankSpace?: SharedValue<number>; | 611 | blankSpace?: SharedValue<number>; |
| ... | @@ -376,22 +618,65 @@ index dd222b57bc7a71729524670bad3812f30920bd73..40249a4357991b7a9c69b87214db2ceb | ... | @@ -376,22 +618,65 @@ index dd222b57bc7a71729524670bad3812f30920bd73..40249a4357991b7a9c69b87214db2ceb |
| 376 | + * Default is `0`. | 618 | + * Default is `0`. |
| 377 | + */ | 619 | + */ |
| 378 | + adjustedInsetCompensation?: number; | 620 | + adjustedInsetCompensation?: number; |
| 621 | + /** Leading inset UIKit adds outside raw contentInset. Offset math only. */ | ||
| 622 | + adjustedStartInsetCompensation?: number; | ||
| 379 | /** | 623 | /** |
| 380 | * Fires whenever the effective content inset changes — the static `contentInset` | 624 | * Fires whenever the effective content inset changes — the static `contentInset` |
| 381 | * prop combined with the dynamic keyboard-driven padding. | 625 | * prop combined with the dynamic keyboard-driven padding. |
| 626 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 627 | index 37eacd10857cd70c51b3468c4b466e4959d0a415..3d77ab27c614f44b64da8270823942cb670b097b 100644 | ||
| 628 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 629 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/helpers.ts | ||
| 630 | @@ -245,6 +245,7 @@ export const computeIOSContentOffset = ( | ||
| 631 | layoutHeight: number, | ||
| 632 | inverted: boolean, | ||
| 633 | totalPaddingForMaxScroll?: number, | ||
| 634 | + startInsetCompensation: number = 0, | ||
| 635 | ): number => { | ||
| 636 | "worklet"; | ||
| 637 | |||
| 638 | @@ -262,7 +263,14 @@ export const computeIOSContentOffset = ( | ||
| 639 | ); | ||
| 640 | } | ||
| 641 | |||
| 642 | - const maxScroll = Math.max(contentHeight - layoutHeight + paddingForMax, 0); | ||
| 643 | + const minScroll = -startInsetCompensation; | ||
| 644 | + const maxScroll = Math.max( | ||
| 645 | + contentHeight - layoutHeight + paddingForMax, | ||
| 646 | + minScroll, | ||
| 647 | + ); | ||
| 648 | |||
| 649 | - return Math.min(Math.max(keyboardHeight + relativeScroll, 0), maxScroll); | ||
| 650 | + return Math.min( | ||
| 651 | + Math.max(keyboardHeight + relativeScroll, minScroll), | ||
| 652 | + maxScroll, | ||
| 653 | + ); | ||
| 654 | }; | ||
| 382 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | 655 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts |
| 383 | index 560df54bae1a8c41a2e9ac0e2a8d2fd9b843968a..a0cb412692cf47fee372a01132e6a670b59bcd66 100644 | 656 | index 560df54bae1a8c41a2e9ac0e2a8d2fd9b843968a..c375a8c4cc889c2fc2c0c1034788c605633192ed 100644 |
| 384 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | 657 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts |
| 385 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts | 658 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/index.ios.ts |
| 386 | @@ -43,6 +43,7 @@ function useChatKeyboard( | 659 | @@ -43,6 +43,8 @@ function useChatKeyboard( |
| 387 | offset, | 660 | offset, |
| 388 | blankSpace, | 661 | blankSpace, |
| 389 | extraContentPadding, | 662 | extraContentPadding, |
| 390 | + adjustedInsetCompensation, | 663 | + adjustedInsetCompensation, |
| 664 | + adjustedStartInsetCompensation, | ||
| 391 | } = options; | 665 | } = options; |
| 392 | 666 | ||
| 393 | const padding = useSharedValue(0); | 667 | const padding = useSharedValue(0); |
| 394 | @@ -104,10 +105,12 @@ function useChatKeyboard( | 668 | @@ -79,6 +81,10 @@ function useChatKeyboard( |
| 669 | offset, | ||
| 670 | ); | ||
| 671 | |||
| 672 | + if (e.duration === 0) { | ||
| 673 | + currentHeight.value = effective; | ||
| 674 | + } | ||
| 675 | + | ||
| 676 | const atEnd = isScrollAtEnd( | ||
| 677 | scroll.value, | ||
| 678 | layout.value.height, | ||
| 679 | @@ -104,10 +110,12 @@ function useChatKeyboard( | ||
| 395 | effective, | 680 | effective, |
| 396 | minimumPaddingAbsorbed, | 681 | minimumPaddingAbsorbed, |
| 397 | ); | 682 | ); |
| ... | @@ -408,50 +693,131 @@ index 560df54bae1a8c41a2e9ac0e2a8d2fd9b843968a..a0cb412692cf47fee372a01132e6a670 | ... | @@ -408,50 +693,131 @@ index 560df54bae1a8c41a2e9ac0e2a8d2fd9b843968a..a0cb412692cf47fee372a01132e6a670 |
| 408 | 693 | ||
| 409 | // persistent mode: when keyboard shrinks, clamp to valid range | 694 | // persistent mode: when keyboard shrinks, clamp to valid range |
| 410 | if ( | 695 | if ( |
| 411 | @@ -242,7 +245,7 @@ function useChatKeyboard( | 696 | @@ -130,11 +138,11 @@ function useChatKeyboard( |
| 697 | } else { | ||
| 698 | const maxScroll = Math.max( | ||
| 699 | size.value.height - layout.value.height + actualTotalPadding, | ||
| 700 | - 0, | ||
| 701 | + -adjustedStartInsetCompensation, | ||
| 702 | ); | ||
| 703 | |||
| 704 | contentOffsetY.value = Math.max( | ||
| 705 | - 0, | ||
| 706 | + -adjustedStartInsetCompensation, | ||
| 707 | Math.min(scroll.value, maxScroll), | ||
| 708 | ); | ||
| 709 | } | ||
| 710 | @@ -165,11 +173,11 @@ function useChatKeyboard( | ||
| 711 | } else { | ||
| 712 | const maxScroll = Math.max( | ||
| 713 | size.value.height - layout.value.height + actualTotalPadding, | ||
| 714 | - 0, | ||
| 715 | + -adjustedStartInsetCompensation, | ||
| 716 | ); | ||
| 717 | |||
| 718 | contentOffsetY.value = Math.max( | ||
| 719 | - 0, | ||
| 720 | + -adjustedStartInsetCompensation, | ||
| 721 | Math.min(scroll.value, maxScroll), | ||
| 722 | ); | ||
| 723 | } | ||
| 724 | @@ -219,12 +227,34 @@ function useChatKeyboard( | ||
| 725 | layout.value.height, | ||
| 726 | inverted, | ||
| 727 | actualTotalPadding, | ||
| 728 | + adjustedStartInsetCompensation, | ||
| 729 | ); | ||
| 730 | }, | ||
| 731 | - onMove: () => { | ||
| 732 | + onMove: (e) => { | ||
| 733 | "worklet"; | ||
| 734 | |||
| 735 | - // iOS doesn't need per-frame updates (contentOffset handles it) | ||
| 736 | + if (freeze.value) { | ||
| 737 | + return; | ||
| 738 | + } | ||
| 739 | + | ||
| 740 | + currentHeight.value = getEffectiveHeight( | ||
| 741 | + e.height, | ||
| 742 | + targetKeyboardHeight.value, | ||
| 743 | + offset, | ||
| 744 | + ); | ||
| 745 | + }, | ||
| 746 | + onInteractive: (e) => { | ||
| 747 | + "worklet"; | ||
| 748 | + | ||
| 749 | + if (freeze.value) { | ||
| 750 | + return; | ||
| 751 | + } | ||
| 752 | + | ||
| 753 | + currentHeight.value = getEffectiveHeight( | ||
| 754 | + e.height, | ||
| 755 | + targetKeyboardHeight.value, | ||
| 756 | + offset, | ||
| 757 | + ); | ||
| 758 | }, | ||
| 759 | onEnd: (e) => { | ||
| 760 | "worklet"; | ||
| 761 | @@ -239,10 +269,18 @@ function useChatKeyboard( | ||
| 762 | offset, | ||
| 763 | ); | ||
| 764 | |||
| 765 | + currentHeight.value = effective; | ||
| 412 | padding.value = effective; | 766 | padding.value = effective; |
| 413 | }, | 767 | }, |
| 414 | }, | 768 | }, |
| 415 | - [inverted, keyboardLiftBehavior, offset, extraContentPadding], | 769 | - [inverted, keyboardLiftBehavior, offset, extraContentPadding], |
| 416 | + [inverted, keyboardLiftBehavior, offset, extraContentPadding, adjustedInsetCompensation], | 770 | + [ |
| 771 | + inverted, | ||
| 772 | + keyboardLiftBehavior, | ||
| 773 | + offset, | ||
| 774 | + extraContentPadding, | ||
| 775 | + adjustedInsetCompensation, | ||
| 776 | + adjustedStartInsetCompensation, | ||
| 777 | + ], | ||
| 417 | ); | 778 | ); |
| 418 | 779 | ||
| 419 | return { | 780 | return { |
| 420 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | 781 | diff --git a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts |
| 421 | index 02abf5cd9490900826678175462b234e07e30e92..3f261fa8f1913a79fa1de2004bbac20415a89acc 100644 | 782 | index 02abf5cd9490900826678175462b234e07e30e92..4ee0ddd65e0b105568ab43b73997af4be8adae00 100644 |
| 422 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | 783 | --- a/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts |
| 423 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts | 784 | +++ b/src/components/KeyboardChatScrollView/useChatKeyboard/types.ts |
| 424 | @@ -11,6 +11,8 @@ type UseChatKeyboardOptions = { | 785 | @@ -11,6 +11,10 @@ type UseChatKeyboardOptions = { |
| 425 | blankSpace: SharedValue<number>; | 786 | blankSpace: SharedValue<number>; |
| 426 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ | 787 | /** Extra content padding shared value — needed on iOS to correctly clamp contentOffset. */ |
| 427 | extraContentPadding: SharedValue<number>; | 788 | extraContentPadding: SharedValue<number>; |
| 428 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | 789 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ |
| 429 | + adjustedInsetCompensation: number; | 790 | + adjustedInsetCompensation: number; |
| 791 | + /** Leading inset UIKit adds beyond the raw contentInset. Offset math only. */ | ||
| 792 | + adjustedStartInsetCompensation: number; | ||
| 430 | }; | 793 | }; |
| 431 | 794 | ||
| 432 | type UseChatKeyboardReturn = { | 795 | type UseChatKeyboardReturn = { |
| 433 | diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | 796 | diff --git a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts |
| 434 | index 833acbe78f1b1245251ddd3431d6546decdd0ade..49d679446b03199217faa16a503d8d15e83a29b9 100644 | 797 | index 833acbe78f1b1245251ddd3431d6546decdd0ade..8f7c10cb6ab392f10df4ca43549b89c25844f0b8 100644 |
| 435 | --- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | 798 | --- a/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts |
| 436 | +++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts | 799 | +++ b/src/components/KeyboardChatScrollView/useExtraContentPadding/index.ts |
| 437 | @@ -16,6 +16,8 @@ type UseExtraContentPaddingOptions = { | 800 | @@ -16,6 +16,10 @@ type UseExtraContentPaddingOptions = { |
| 438 | keyboardPadding: SharedValue<number>; | 801 | keyboardPadding: SharedValue<number>; |
| 439 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ | 802 | /** Minimum inset floor — used to absorb keyboard and extraContentPadding changes. */ |
| 440 | blankSpace: SharedValue<number>; | 803 | blankSpace: SharedValue<number>; |
| 441 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ | 804 | + /** Extra bottom inset UIKit adds beyond the raw contentInset (safe area). Offset math only. */ |
| 442 | + adjustedInsetCompensation: number; | 805 | + adjustedInsetCompensation: number; |
| 806 | + /** Leading inset UIKit adds beyond raw contentInset. Offset math only. */ | ||
| 807 | + adjustedStartInsetCompensation: number; | ||
| 443 | /** Current vertical scroll offset. */ | 808 | /** Current vertical scroll offset. */ |
| 444 | scroll: SharedValue<number>; | 809 | scroll: SharedValue<number>; |
| 445 | /** Visible viewport dimensions. */ | 810 | /** Visible viewport dimensions. */ |
| 446 | @@ -49,6 +51,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | 811 | @@ -49,6 +53,8 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { |
| 447 | extraContentPadding, | 812 | extraContentPadding, |
| 448 | keyboardPadding, | 813 | keyboardPadding, |
| 449 | blankSpace, | 814 | blankSpace, |
| 450 | + adjustedInsetCompensation, | 815 | + adjustedInsetCompensation, |
| 816 | + adjustedStartInsetCompensation, | ||
| 451 | scroll, | 817 | scroll, |
| 452 | layout, | 818 | layout, |
| 453 | size, | 819 | size, |
| 454 | @@ -97,14 +100,12 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | 820 | @@ -97,14 +103,12 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { |
| 455 | } | 821 | } |
| 456 | 822 | ||
| 457 | // Compute effective delta considering blankSpace floor | 823 | // Compute effective delta considering blankSpace floor |
| ... | @@ -472,12 +838,32 @@ index 833acbe78f1b1245251ddd3431d6546decdd0ade..49d679446b03199217faa16a503d8d15 | ... | @@ -472,12 +838,32 @@ index 833acbe78f1b1245251ddd3431d6546decdd0ade..49d679446b03199217faa16a503d8d15 |
| 472 | const effectiveDelta = currentTotal - previousTotal; | 838 | const effectiveDelta = currentTotal - previousTotal; |
| 473 | 839 | ||
| 474 | if (effectiveDelta === 0) { | 840 | if (effectiveDelta === 0) { |
| 475 | @@ -146,7 +147,7 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { | 841 | @@ -137,16 +141,25 @@ function useExtraContentPadding(options: UseExtraContentPaddingOptions): void { |
| 842 | |||
| 843 | scrollToTarget(target); | ||
| 844 | } else { | ||
| 845 | + const minScroll = -adjustedStartInsetCompensation; | ||
| 846 | const maxScroll = Math.max( | ||
| 847 | size.value.height - layout.value.height + currentTotal, | ||
| 848 | - 0, | ||
| 849 | + minScroll, | ||
| 850 | + ); | ||
| 851 | + const target = Math.max( | ||
| 852 | + minScroll, | ||
| 853 | + Math.min(scroll.value + effectiveDelta, maxScroll), | ||
| 854 | ); | ||
| 855 | - const target = Math.min(scroll.value + effectiveDelta, maxScroll); | ||
| 856 | |||
| 476 | scrollToTarget(target); | 857 | scrollToTarget(target); |
| 477 | } | 858 | } |
| 478 | }, | 859 | }, |
| 479 | - [inverted, keyboardLiftBehavior], | 860 | - [inverted, keyboardLiftBehavior], |
| 480 | + [inverted, keyboardLiftBehavior, adjustedInsetCompensation], | 861 | + [ |
| 862 | + inverted, | ||
| 863 | + keyboardLiftBehavior, | ||
| 864 | + adjustedInsetCompensation, | ||
| 865 | + adjustedStartInsetCompensation, | ||
| 866 | + ], | ||
| 481 | ); | 867 | ); |
| 482 | } | 868 | } |
| 483 | 869 |
pnpm-lock.yaml+8-8| ... | @@ -86,13 +86,13 @@ patchedDependencies: | ... | @@ -86,13 +86,13 @@ patchedDependencies: |
| 86 | '@effect/vitest@4.0.0-beta.103': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b | 86 | '@effect/vitest@4.0.0-beta.103': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b |
| 87 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 | 87 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 |
| 88 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 | 88 | '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368 |
| 89 | '@legendapp/list@3.3.5': 064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706 | 89 | '@legendapp/list@3.3.5': f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b |
| 90 | '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa | 90 | '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa |
| 91 | '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd | 91 | '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd |
| 92 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 | 92 | '@react-navigation/native-stack@7.17.6': e667c3cef8c78bb9ff4882ee5bd23a432247b843060a9499eb5f07e9e2295552 |
| 93 | effect@4.0.0-beta.103: af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6 | 93 | effect@4.0.0-beta.103: af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6 |
| 94 | react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 | 94 | react-native-gesture-handler@2.32.0: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 |
| 95 | react-native-keyboard-controller@1.21.13: 20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008 | 95 | react-native-keyboard-controller@1.21.13: 6e4339347bc5bb3c9ea67d85ff5c814058b211c5750f247aba59d07869a2e787 |
| 96 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 | 96 | react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 |
| 97 | react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006 | 97 | react-native-screens@4.26.2: 149bef30a66351ea9b26b42f87b78c539cb52b880f2387bb323ec80dcac84006 |
| 98 | uniwind@1.11.0: 329a77525509623d763b738152dbd00ab392cdcdd9fb6ed2b4a20e086e437196 | 98 | uniwind@1.11.0: 329a77525509623d763b738152dbd00ab392cdcdd9fb6ed2b4a20e086e437196 |
| ... | @@ -228,7 +228,7 @@ importers: | ... | @@ -228,7 +228,7 @@ importers: |
| 228 | version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(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) | 228 | version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(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) |
| 229 | '@legendapp/list': | 229 | '@legendapp/list': |
| 230 | specifier: 'catalog:' | 230 | specifier: 'catalog:' |
| 231 | version: 3.3.5(patch_hash=064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706)(react-dom@19.2.3(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) | 231 | version: 3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.3(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) |
| 232 | '@noble/curves': | 232 | '@noble/curves': |
| 233 | specifier: 'catalog:' | 233 | specifier: 'catalog:' |
| 234 | version: 1.9.1 | 234 | version: 1.9.1 |
| ... | @@ -402,7 +402,7 @@ importers: | ... | @@ -402,7 +402,7 @@ importers: |
| 402 | 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) | 402 | 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) |
| 403 | react-native-keyboard-controller: | 403 | react-native-keyboard-controller: |
| 404 | specifier: 1.21.13 | 404 | specifier: 1.21.13 |
| 405 | version: 1.21.13(patch_hash=20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008)(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) | 405 | 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) |
| 406 | react-native-nitro-markdown: | 406 | react-native-nitro-markdown: |
| 407 | specifier: ^0.5.0 | 407 | specifier: ^0.5.0 |
| 408 | 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) | 408 | 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) |
| ... | @@ -560,7 +560,7 @@ importers: | ... | @@ -560,7 +560,7 @@ importers: |
| 560 | version: 0.9.0 | 560 | version: 0.9.0 |
| 561 | '@legendapp/list': | 561 | '@legendapp/list': |
| 562 | specifier: 'catalog:' | 562 | specifier: 'catalog:' |
| 563 | version: 3.3.5(patch_hash=064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) | 563 | version: 3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) |
| 564 | '@lexical/react': | 564 | '@lexical/react': |
| 565 | specifier: ^0.41.0 | 565 | specifier: ^0.41.0 |
| 566 | version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) | 566 | version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) |
| ... | @@ -12828,7 +12828,7 @@ snapshots: | ... | @@ -12828,7 +12828,7 @@ snapshots: |
| 12828 | dependencies: | 12828 | dependencies: |
| 12829 | jsbi: 4.3.2 | 12829 | jsbi: 4.3.2 |
| 12830 | 12830 | ||
| 12831 | '@legendapp/list@3.3.5(patch_hash=064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706)(react-dom@19.2.3(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)': | 12831 | '@legendapp/list@3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.3(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)': |
| 12832 | dependencies: | 12832 | dependencies: |
| 12833 | react: 19.2.3 | 12833 | react: 19.2.3 |
| 12834 | use-sync-external-store: 1.6.0(react@19.2.3) | 12834 | use-sync-external-store: 1.6.0(react@19.2.3) |
| ... | @@ -12836,7 +12836,7 @@ snapshots: | ... | @@ -12836,7 +12836,7 @@ snapshots: |
| 12836 | react-dom: 19.2.3(react@19.2.3) | 12836 | react-dom: 19.2.3(react@19.2.3) |
| 12837 | 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) | 12837 | 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) |
| 12838 | 12838 | ||
| 12839 | '@legendapp/list@3.3.5(patch_hash=064530db83875fa671559a81ae42dc159726dc2dd6ec4c982da44ff7c7a74706)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': | 12839 | '@legendapp/list@3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': |
| 12840 | dependencies: | 12840 | dependencies: |
| 12841 | react: 19.2.6 | 12841 | react: 19.2.6 |
| 12842 | use-sync-external-store: 1.6.0(react@19.2.6) | 12842 | use-sync-external-store: 1.6.0(react@19.2.6) |
| ... | @@ -19356,7 +19356,7 @@ snapshots: | ... | @@ -19356,7 +19356,7 @@ snapshots: |
| 19356 | react: 19.2.3 | 19356 | react: 19.2.3 |
| 19357 | 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) | 19357 | 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) |
| 19358 | 19358 | ||
| 19359 | react-native-keyboard-controller@1.21.13(patch_hash=20be72c84d74253acdcfefbc6defe36dc396944f1a44cab2bdd0e3cd572ae008)(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): | 19359 | 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): |
| 19360 | dependencies: | 19360 | dependencies: |
| 19361 | react: 19.2.3 | 19361 | react: 19.2.3 |
| 19362 | 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) | 19362 | 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) |