| @@ -1,6 +1,6 @@ |
| 1 | import * as Haptics from "expo-haptics"; | 1 | import * as Haptics from "expo-haptics"; |
| 2 | import { KeyboardAwareLegendList } from "@legendapp/list/keyboard"; | 2 | import { KeyboardAwareLegendList } from "@legendapp/list/keyboard"; |
| 3 | import { type LegendListRef } from "@legendapp/list/react-native"; | 3 | import { useViewabilityAmount, type LegendListRef } from "@legendapp/list/react-native"; |
| 4 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; | 4 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 5 | import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; | 5 | import { canForkProjectedAssistantItem } from "@t3tools/client-runtime/state/thread-workflows"; |
| 6 | import { | 6 | import { |
| @@ -79,7 +79,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; · 72 unmodified lines |
| 79 | import Animated, { | 79 | import Animated, { |
| 80 | FadeIn, | 80 | FadeIn, |
| 81 | FadeInUp, | 81 | FadeInUp, |
| 82 | FadeOut, | | |
| 83 | LinearTransition, | 82 | LinearTransition, |
| 84 | type SharedValue, | 83 | type SharedValue, |
| 85 | } from "react-native-reanimated"; | 84 | } from "react-native-reanimated"; |
| @@ -150,13 +149,19 @@ import { · 64 unmodified lines |
| 150 | type ThreadFeedLiveFollowEvent, | 149 | type ThreadFeedLiveFollowEvent, |
| 151 | } from "./thread-feed-live-follow"; | 150 | } from "./thread-feed-live-follow"; |
| 152 | import { | 151 | import { |
| | 152 | ThreadDisclosureChevron, |
| 153 | THREAD_DISCLOSURE_TRANSITION_MS, | 153 | THREAD_DISCLOSURE_TRANSITION_MS, |
| 154 | ThreadWorkGroupToggle, | 154 | ThreadWorkGroupToggle, |
| 155 | ThreadWorkLog, | 155 | ThreadWorkLog, |
| 156 | } from "./thread-work-log"; | 156 | } from "./thread-work-log"; |
| 157 | import { resolveThreadFeedFixedItemSize } from "./thread-feed-item-size"; | 157 | import { resolveThreadFeedFixedItemSize } from "./thread-feed-item-size"; |
| 158 | import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; | 158 | import { useMarkdownCodeHighlight } from "./markdownCodeHighlightState"; |
| 159 | import { assetEnvironment, useAssetUrl, useAssetUrlState } from "../../state/assets"; | 159 | import { |
| | 160 | assetEnvironment, |
| | 161 | useAssetUrl, |
| | 162 | useAssetUrlState, |
| | 163 | useRefreshAssetUrl, |
| | 164 | } from "../../state/assets"; |
| 160 | import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; | 165 | import { useAtomQueryRunner } from "../../state/use-atom-query-runner"; |
| 161 | import { usePreparedConnection } from "../../state/session"; | 166 | import { usePreparedConnection } from "../../state/session"; |
| 162 | import * as Option from "effect/Option"; | 167 | import * as Option from "effect/Option"; |
| @@ -191,9 +196,10 @@ function formatMessageTime(input: string): string { · 28 unmodified lines |
| 191 | } | 196 | } |
| 192 | | 197 | |
| 193 | const THREAD_FEED_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_TRANSITION_MS); | 198 | const THREAD_FEED_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_TRANSITION_MS); |
| 194 | const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.duration(140); | 199 | // Let neighboring rows move out of the new rows' space before showing their text. |
| 195 | const THREAD_FEED_DISCLOSURE_EXIT_TRANSITION = FadeOut.duration(120); | 200 | const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.delay( |
| 196 | const EMPTY_DISCLOSURE_ENTRY_IDS: ReadonlySet<string> = new Set(); | 201 | THREAD_DISCLOSURE_TRANSITION_MS, |
| | 202 | ).duration(140); |
| 197 | | 203 | |
| 198 | // Entering animations must only play for rows born just now — LegendList | 204 | // Entering animations must only play for rows born just now — LegendList |
| 199 | // remounts rows when they scroll back into view, and replaying an entrance for | 205 | // remounts rows when they scroll back into view, and replaying an entrance for |
| @@ -340,6 +346,7 @@ function MessageAttachmentFile(props: { · 140 unmodified lines |
| 340 | }) { | 346 | }) { |
| 341 | const sourceIdentifier = useId(); | 347 | const sourceIdentifier = useId(); |
| 342 | const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, { | 348 | const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, { |
| | 349 | refresh: true, |
| 343 | reportFailure: false, | 350 | reportFailure: false, |
| 344 | }); | 351 | }); |
| 345 | const preparedConnection = usePreparedConnection(props.environmentId); | 352 | const preparedConnection = usePreparedConnection(props.environmentId); |
| @@ -557,9 +564,11 @@ function ThreadMarkdownImageView(props: { · 211 unmodified lines |
| 557 | readonly sourceKey: string; | 564 | readonly sourceKey: string; |
| 558 | readonly unavailable: boolean; | 565 | readonly unavailable: boolean; |
| 559 | readonly alt: string | null; | 566 | readonly alt: string | null; |
| | 567 | readonly actionsSource?: MediaActionsSource; |
| 560 | readonly onPressPreview: (source: FilePreviewSource) => void; | 568 | readonly onPressPreview: (source: FilePreviewSource) => void; |
| 561 | }) { | 569 | }) { |
| 562 | const sourceIdentifier = useId(); | 570 | const sourceIdentifier = useId(); |
| | 571 | const mediaActions = useMediaActions(props.actionsSource); |
| 563 | const [availableWidth, setAvailableWidth] = useState(0); | 572 | const [availableWidth, setAvailableWidth] = useState(0); |
| 564 | const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); | 573 | const [sourceSize, setSourceSize] = useState<{ width: number; height: number } | null>(null); |
| 565 | const [failedUri, setFailedUri] = useState<string | null>(null); | 574 | const [failedUri, setFailedUri] = useState<string | null>(null); |
| @@ -602,36 +611,51 @@ function ThreadMarkdownImageView(props: { · 36 unmodified lines |
| 602 | ) : ( | 611 | ) : ( |
| 603 | <ActivityIndicator /> | 612 | <ActivityIndicator /> |
| 604 | )} | 613 | )} |
| | 614 | {props.actionsSource ? ( |
| | 615 | <View className="absolute right-1 top-1"> |
| | 616 | <MediaActionsMenu media={mediaActions} /> |
| | 617 | </View> |
| | 618 | ) : null} |
| 605 | </View> | 619 | </View> |
| 606 | ) : ( | 620 | ) : ( |
| 607 | <PresentationSource identifier={sourceIdentifier} style={{ alignSelf: "flex-start" }}> | 621 | <PresentationSource identifier={sourceIdentifier} style={{ alignSelf: "flex-start" }}> |
| 608 | <Pressable | 622 | <View> |
| 609 | accessibilityRole="imagebutton" | 623 | <MediaActionsMenu media={mediaActions}> |
| 610 | accessibilityLabel={props.alt ?? "Markdown image"} | 624 | <Pressable |
| 611 | onPress={() => | 625 | accessibilityRole="imagebutton" |
| 612 | props.onPressPreview({ | 626 | accessibilityLabel={props.alt ?? "Markdown image"} |
| 613 | kind: "image", | 627 | onPress={() => |
| 614 | uri: props.uri!, | 628 | props.onPressPreview({ |
| 615 | name: props.alt ?? "Image", | 629 | kind: "image", |
| 616 | sourceIdentifier, | 630 | uri: props.uri!, |
| 617 | }) | 631 | name: props.alt ?? "Image", |
| 618 | } | 632 | sourceIdentifier, |
| 619 | style={{ alignSelf: "flex-start" }} | 633 | actionsSource: props.actionsSource, |
| 620 | > | 634 | }) |
| 621 | <View | 635 | } |
| 622 | className="items-center justify-center overflow-hidden rounded-[10px] bg-md-code-bg" | 636 | style={{ alignSelf: "flex-start" }} |
| 623 | style={{ | 637 | > |
| 624 | ...frameStyle, | 638 | <View |
| 625 | }} | 639 | className="items-center justify-center overflow-hidden rounded-[10px] bg-md-code-bg" |
| 626 | > | 640 | style={{ |
| 627 | <ThreadMarkdownImageRequest | 641 | ...frameStyle, |
| 628 | key={props.uri} | 642 | }} |
| 629 | uri={props.uri} | 643 | > |
| 630 | onLoad={setSourceSize} | 644 | <ThreadMarkdownImageRequest |
| 631 | onError={() => setFailedUri(props.uri)} | 645 | key={props.uri} |
| 632 | /> | 646 | uri={props.uri} |
| 633 | </View> | 647 | onLoad={setSourceSize} |
| 634 | </Pressable> | 648 | onError={() => setFailedUri(props.uri)} |
| | 649 | /> |
| | 650 | </View> |
| | 651 | </Pressable> |
| | 652 | </MediaActionsMenu> |
| | 653 | {props.actionsSource ? ( |
| | 654 | <View className="absolute right-1 top-1"> |
| | 655 | <MediaActionsMenu media={mediaActions} /> |
| | 656 | </View> |
| | 657 | ) : null} |
| | 658 | </View> |
| 635 | </PresentationSource> | 659 | </PresentationSource> |
| 636 | )} | 660 | )} |
| 637 | {props.alt ? ( | 661 | {props.alt ? ( |
| @@ -678,9 +702,10 @@ function ThreadMarkdownImageRequest(props: { · 40 unmodified lines |
| 678 | /** Environment-hosted image that loads through a signed asset URL. */ | 702 | /** Environment-hosted image that loads through a signed asset URL. */ |
| 679 | function ThreadMarkdownImage(props: { | 703 | function ThreadMarkdownImage(props: { |
| 680 | readonly environmentId: EnvironmentId; | 704 | readonly environmentId: EnvironmentId; |
| 681 | readonly resource: Extract<AssetResource, { readonly _tag: "attachment" | "workspace-file" }>; | 705 | readonly resource: Extract<AssetResource, { readonly _tag: "attachment" | "media-file" }>; |
| 682 | readonly alt: string | null; | 706 | readonly alt: string | null; |
| 683 | readonly srcFragment?: string; | 707 | readonly srcFragment?: string; |
| | 708 | readonly actionsSource?: MediaActionsSource; |
| 684 | readonly onPressPreview: (source: FilePreviewSource) => void; | 709 | readonly onPressPreview: (source: FilePreviewSource) => void; |
| 685 | }) { | 710 | }) { |
| 686 | const assetUrl = useAssetUrlState(props.environmentId, props.resource); | 711 | const assetUrl = useAssetUrlState(props.environmentId, props.resource); |
| @@ -695,11 +720,59 @@ function ThreadMarkdownImage(props: { · 8 unmodified lines |
| 695 | } | 720 | } |
| 696 | unavailable={assetUrl._tag === "Failure"} | 721 | unavailable={assetUrl._tag === "Failure"} |
| 697 | alt={props.alt} | 722 | alt={props.alt} |
| | 723 | actionsSource={props.actionsSource} |
| 698 | onPressPreview={props.onPressPreview} | 724 | onPressPreview={props.onPressPreview} |
| 699 | /> | 725 | /> |
| 700 | ); | 726 | ); |
| 701 | } | 727 | } |
| 702 | | 728 | |
| | 729 | const ThreadMediaVisibleContext = createContext(false); |
| | 730 | // LegendList only computes hook visibility when the list has a viewability config. |
| | 731 | const THREAD_MEDIA_VIEWABILITY_CONFIG = { itemVisiblePercentThreshold: 0 }; |
| | 732 | |
| | 733 | function ThreadMediaVisibility(props: { readonly children: ReactNode }) { |
| | 734 | const [visible, setVisible] = useState(false); |
| | 735 | useViewabilityAmount<ThreadFeedEntry>( |
| | 736 | useCallback((token) => setVisible(token.sizeVisible > 0), []), |
| | 737 | ); |
| | 738 | return <ThreadMediaVisibleContext value={visible}>{props.children}</ThreadMediaVisibleContext>; |
| | 739 | } |
| | 740 | |
| | 741 | function ThreadMarkdownVideo(props: { |
| | 742 | readonly source: MediaVideoPreviewSource; |
| | 743 | readonly onExpand: (source: MediaVideoPreviewSource) => void; |
| | 744 | }) { |
| | 745 | const { source } = props; |
| | 746 | const visible = useContext(ThreadMediaVisibleContext); |
| | 747 | const thumbnailKey = mediaVideoThumbnailKey(source); |
| | 748 | const asset = useAssetUrlState( |
| | 749 | "environmentId" in source ? source.environmentId : null, |
| | 750 | "resource" in source ? source.resource : null, |
| | 751 | ); |
| | 752 | const refreshAssetUrl = useRefreshAssetUrl( |
| | 753 | "environmentId" in source ? source.environmentId : null, |
| | 754 | "resource" in source ? source.resource : null, |
| | 755 | ); |
| | 756 | const uri = mediaVideoPreviewUri(source, asset._tag === "Success" ? asset.url : null); |
| | 757 | return ( |
| | 758 | <MediaVideoPlayer |
| | 759 | key={thumbnailKey} |
| | 760 | uri={uri} |
| | 761 | resolvePlaybackUri={ |
| | 762 | "resource" in source |
| | 763 | ? async () => mediaVideoPreviewUri(source, await refreshAssetUrl()) |
| | 764 | : undefined |
| | 765 | } |
| | 766 | name={source.name} |
| | 767 | thumbnailKey={thumbnailKey} |
| | 768 | thumbnailVisible={visible} |
| | 769 | unavailable={"resource" in source && asset._tag === "Failure"} |
| | 770 | actionsSource={source.actionsSource} |
| | 771 | onExpand={() => props.onExpand(source)} |
| | 772 | /> |
| | 773 | ); |
| | 774 | } |
| | 775 | |
| 703 | function ThreadMarkdownImageUnavailable(props: { readonly alt: string | null }) { | 776 | function ThreadMarkdownImageUnavailable(props: { readonly alt: string | null }) { |
| 704 | return ( | 777 | return ( |
| 705 | <ThreadMarkdownImageView | 778 | <ThreadMarkdownImageView |
| @@ -1287,21 +1360,16 @@ function useMarkdownStyles( · 581 unmodified lines |
| 1287 | title: node.title ?? null, | 1360 | title: node.title ?? null, |
| 1288 | }) ?? undefined) | 1361 | }) ?? undefined) |
| 1289 | : undefined, | 1362 | : undefined, |
| 1290 | code_inline: ({ content }) => { | 1363 | code_inline: ({ content }) => ( |
| 1291 | const value = content ?? ""; | 1364 | <MarkdownInlineCode |
| 1292 | return ( | 1365 | content={content ?? ""} |
| 1293 | <NativeText | 1366 | textColor={inlineTextColor} |
| 1294 | className="font-mono" | 1367 | codeColor={inlineCodeTextColor} |
| 1295 | style={{ | 1368 | fontSize={markdownFontSizes.codeBlockFontSize} |
| 1296 | color: inlineCodeTextColor, | 1369 | lineHeight={markdownFontSizes.bodyLineHeight} |
| 1297 | fontSize: markdownFontSizes.codeBlockFontSize, | 1370 | onLinkPress={onLinkPress} |
| 1298 | lineHeight: markdownFontSizes.bodyLineHeight, | 1371 | /> |
| 1299 | }} | 1372 | ), |
| 1300 | > | | |
| 1301 | {value} | | |
| 1302 | </NativeText> | | |
| 1303 | ); | | |
| 1304 | }, | | |
| 1305 | ...(preserveSoftBreaks | 1373 | ...(preserveSoftBreaks |
| 1306 | ? { | 1374 | ? { |
| 1307 | soft_break: () => <NativeText>{"\n"}</NativeText>, | 1375 | soft_break: () => <NativeText>{"\n"}</NativeText>, |
| @@ -1504,7 +1572,7 @@ function renderFeedEntry( · 196 unmodified lines |
| 1504 | accessibilityState={{ expanded: entry.expanded }} | 1572 | accessibilityState={{ expanded: entry.expanded }} |
| 1505 | onPress={() => props.onToggleTurnFold(entry.runId)} | 1573 | onPress={() => props.onToggleTurnFold(entry.runId)} |
| 1506 | hitSlop={4} | 1574 | hitSlop={4} |
| 1507 | className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2" | 1575 | className="mb-1 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2" |
| 1508 | > | 1576 | > |
| 1509 | <Text | 1577 | <Text |
| 1510 | key={props.workRowSizing.textSizeKey} | 1578 | key={props.workRowSizing.textSizeKey} |
| @@ -1516,8 +1584,7 @@ function renderFeedEntry( · 5 unmodified lines |
| 1516 | expanded={entry.expanded} | 1584 | expanded={entry.expanded} |
| 1517 | collapsedDirection="right" | 1585 | collapsedDirection="right" |
| 1518 | size={15} | 1586 | size={15} |
| 1519 | tintColorClassName={"accent-icon-subtle"} | 1587 | tintColor={iconSubtleColor} |
| 1520 | type="monochrome" | | |
| 1521 | /> | 1588 | /> |
| 1522 | </Pressable> | 1589 | </Pressable> |
| 1523 | ); | 1590 | ); |
| @@ -2092,7 +2159,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 568 unmodified lines |
| 2092 | const [endFollowEnabled, setEndFollowEnabled] = useState(true); | 2159 | const [endFollowEnabled, setEndFollowEnabled] = useState(true); |
| 2093 | const endFollowEnabledRef = useRef(true); | 2160 | const endFollowEnabledRef = useRef(true); |
| 2094 | // A "user scroll session" spans from drag start through the end of its | 2161 | // A "user scroll session" spans from drag start through the end of its |
| 2095 | // momentum; only motion inside a session can break follow, so MVCP | 2162 | // momentum; scroll events only break follow inside that session, so MVCP |
| 2096 | // compensations and programmatic scrolls never strand a follower. | 2163 | // compensations and programmatic scrolls never strand a follower. |
| 2097 | const userScrollSessionRef = useRef(false); | 2164 | const userScrollSessionRef = useRef(false); |
| 2098 | const setEndFollow = useCallback( | 2165 | const setEndFollow = useCallback( |
| @@ -2253,14 +2320,30 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 154 unmodified lines |
| 2253 | ); | 2320 | ); |
| 2254 | const renderMarkdownImage = useCallback<MarkdownImageRenderer>( | 2321 | const renderMarkdownImage = useCallback<MarkdownImageRenderer>( |
| 2255 | (image) => { | 2322 | (image) => { |
| | 2323 | const media = resolveMarkdownMediaPreview(image.href, { |
| | 2324 | environmentId: props.environmentId, |
| | 2325 | threadId: props.threadId, |
| | 2326 | workspaceRoot: props.workspaceRoot, |
| | 2327 | imageEmbed: true, |
| | 2328 | }); |
| | 2329 | if (media?.kind === "video") { |
| | 2330 | return ( |
| | 2331 | <ThreadMarkdownVideo |
| | 2332 | key={image.href} |
| | 2333 | source={{ ...media.source, name: image.alt ?? media.source.name }} |
| | 2334 | onExpand={(source) => setExpandedVideo((current) => current ?? source)} |
| | 2335 | /> |
| | 2336 | ); |
| | 2337 | } |
| 2256 | const imageSource = classifyMarkdownImageSource(image.href, props.workspaceRoot ?? null); | 2338 | const imageSource = classifyMarkdownImageSource(image.href, props.workspaceRoot ?? null); |
| 2257 | if (imageSource._tag === "Direct") { | 2339 | if (imageSource._tag === "Direct") { |
| 2258 | return ( | 2340 | return ( |
| 2259 | <ThreadMarkdownImageView | 2341 | <ThreadMarkdownImageView |
| 2260 | uri={imageSource.uri} | 2342 | uri={normalizeNativeMarkdownUrl(imageSource.uri)} |
| 2261 | sourceKey={imageSource.uri} | 2343 | sourceKey={imageSource.uri} |
| 2262 | unavailable={false} | 2344 | unavailable={false} |
| 2263 | alt={image.alt} | 2345 | alt={image.alt} |
| | 2346 | actionsSource={media?.source.actionsSource} |
| 2264 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} | 2347 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} |
| 2265 | /> | 2348 | /> |
| 2266 | ); | 2349 | ); |
| @@ -2272,12 +2355,13 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 5 unmodified lines |
| 2272 | <ThreadMarkdownImage | 2355 | <ThreadMarkdownImage |
| 2273 | environmentId={props.environmentId} | 2356 | environmentId={props.environmentId} |
| 2274 | resource={{ | 2357 | resource={{ |
| 2275 | _tag: "workspace-file", | 2358 | _tag: "media-file", |
| 2276 | threadId: props.threadId, | 2359 | threadId: props.threadId, |
| 2277 | path: imageSource.path, | 2360 | path: imageSource.path, |
| 2278 | }} | 2361 | }} |
| 2279 | alt={image.alt} | 2362 | alt={image.alt} |
| 2280 | srcFragment={markdownImageSourceFragment(image.href)} | 2363 | srcFragment={markdownImageSourceFragment(image.href)} |
| | 2364 | actionsSource={media?.source.actionsSource} |
| 2281 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} | 2365 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} |
| 2282 | /> | 2366 | /> |
| 2283 | ); | 2367 | ); |
| @@ -2290,12 +2374,26 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 6 unmodified lines |
| 2290 | threadId: props.threadId, | 2374 | threadId: props.threadId, |
| 2291 | workspaceRoot: props.workspaceRoot, | 2375 | workspaceRoot: props.workspaceRoot, |
| 2292 | }); | 2376 | }); |
| | 2377 | const media = viewedImage |
| | 2378 | ? resolveMarkdownMediaPreview(image.href, { |
| | 2379 | environmentId: props.environmentId, |
| | 2380 | threadId: props.threadId, |
| | 2381 | workspaceRoot: props.workspaceRoot, |
| | 2382 | imageEmbed: true, |
| | 2383 | }) |
| | 2384 | : null; |
| | 2385 | const actionsSource = media?.source.actionsSource; |
| 2293 | return viewedImage ? ( | 2386 | return viewedImage ? ( |
| 2294 | <ThreadMarkdownImage | 2387 | <ThreadMarkdownImage |
| 2295 | environmentId={props.environmentId} | 2388 | environmentId={props.environmentId} |
| 2296 | resource={viewedImage.resource} | 2389 | resource={viewedImage.resource} |
| 2297 | alt={viewedImage.alt} | 2390 | alt={viewedImage.alt} |
| 2298 | srcFragment={viewedImage.srcFragment} | 2391 | srcFragment={viewedImage.srcFragment} |
| | 2392 | actionsSource={ |
| | 2393 | actionsSource && "resource" in actionsSource |
| | 2394 | ? { ...actionsSource, resource: viewedImage.resource } |
| | 2395 | : undefined |
| | 2396 | } |
| 2299 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} | 2397 | onPressPreview={(source) => setExpandedFile((current) => current ?? source)} |
| 2300 | /> | 2398 | /> |
| 2301 | ) : null; | 2399 | ) : null; |
| @@ -2572,13 +2670,23 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 270 unmodified lines |
| 2572 | } | 2670 | } |
| 2573 | disclosureSettleFrameRef.current = requestAnimationFrame(() => { | 2671 | disclosureSettleFrameRef.current = requestAnimationFrame(() => { |
| 2574 | disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => { | 2672 | disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => { |
| | 2673 | // A disclosure can leave the reader above the end without a drag. |
| | 2674 | // Reconcile follow before a later layout or resume can re-pin it. |
| | 2675 | const listState = props.listRef.current?.getState(); |
| | 2676 | if (listState) { |
| | 2677 | transitionEndFollow({ |
| | 2678 | type: "disclosure-settled", |
| | 2679 | isAtEnd: listState.isAtEnd, |
| | 2680 | userScrollSessionActive: userScrollSessionRef.current, |
| | 2681 | }); |
| | 2682 | } |
| 2575 | disclosureAnchorKeyRef.current = null; | 2683 | disclosureAnchorKeyRef.current = null; |
| 2576 | setDisclosureToggleSettling(false); | 2684 | setDisclosureToggleSettling(false); |
| 2577 | disclosureSettleFrameRef.current = null; | 2685 | disclosureSettleFrameRef.current = null; |
| 2578 | disclosureSettleSecondFrameRef.current = null; | 2686 | disclosureSettleSecondFrameRef.current = null; |
| 2579 | }); | 2687 | }); |
| 2580 | }); | 2688 | }); |
| 2581 | }, []); | 2689 | }, [props.listRef, transitionEndFollow]); |
| 2582 | | 2690 | |
| 2583 | const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string | null) => { | 2691 | const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string | null) => { |
| 2584 | disclosureAnchorKeyRef.current = anchorKey; | 2692 | disclosureAnchorKeyRef.current = anchorKey; |
| @@ -2713,44 +2821,41 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 128 unmodified lines |
| 2713 | (info: { item: ThreadFeedEntry; index: number }) => ( | 2821 | (info: { item: ThreadFeedEntry; index: number }) => ( |
| 2714 | <Animated.View | 2822 | <Animated.View |
| 2715 | key={info.item.id} | 2823 | key={info.item.id} |
| 2716 | entering={ | 2824 | entering={disclosureToggleSettling ? THREAD_FEED_DISCLOSURE_ENTER_TRANSITION : undefined} |
| 2717 | disclosureEnteringEntryIds.has(info.item.id) | | |
| 2718 | ? THREAD_FEED_DISCLOSURE_ENTER_TRANSITION | | |
| 2719 | : undefined | | |
| 2720 | } | | |
| 2721 | exiting={THREAD_FEED_DISCLOSURE_EXIT_TRANSITION} | | |
| 2722 | > | 2825 | > |
| 2723 | {renderFeedEntry(info, { | 2826 | <ThreadMediaVisibility> |
| 2724 | environmentId: props.environmentId, | 2827 | {renderFeedEntry(info, { |
| 2725 | threadId: props.threadId, | 2828 | environmentId: props.environmentId, |
| 2726 | copiedRowId, | 2829 | threadId: props.threadId, |
| 2727 | expandedWorkRows, | 2830 | copiedRowId, |
| 2728 | terminalAssistantMessageIds, | 2831 | expandedWorkRows, |
| 2729 | unsettledTurnId, | 2832 | terminalAssistantMessageIds, |
| 2730 | onCopyWorkRow, | 2833 | unsettledTurnId, |
| 2731 | onToggleWorkGroup, | 2834 | onCopyWorkRow, |
| 2732 | onToggleWorkRow, | 2835 | onToggleWorkGroup, |
| 2733 | onToggleTurnFold, | 2836 | onToggleWorkRow, |
| 2734 | onPressPreview, | 2837 | onToggleTurnFold, |
| 2735 | onPressVideo, | 2838 | onPressPreview, |
| 2736 | onMarkdownLinkPress, | 2839 | onPressVideo, |
| 2737 | renderMarkdownImage, | 2840 | onMarkdownLinkPress, |
| 2738 | renderViewedImage, | 2841 | renderMarkdownImage, |
| 2739 | iconSubtleColor, | 2842 | renderViewedImage, |
| 2740 | userBubbleColor, | 2843 | iconSubtleColor, |
| 2741 | markdownStyles, | 2844 | userBubbleColor, |
| 2742 | reviewCommentColors, | 2845 | markdownStyles, |
| 2743 | reviewCommentBubbleWidth, | 2846 | reviewCommentColors, |
| 2744 | userBubbleMaxWidth, | 2847 | reviewCommentBubbleWidth, |
| 2745 | threadTitle: props.threadTitle, | 2848 | userBubbleMaxWidth, |
| 2746 | skills: props.skills, | 2849 | threadTitle: props.threadTitle, |
| 2747 | workspaceRoot: props.workspaceRoot, | 2850 | skills: props.skills, |
| 2748 | })} | 2851 | workspaceRoot: props.workspaceRoot, |
| | 2852 | })} |
| | 2853 | </ThreadMediaVisibility> |
| 2749 | </Animated.View> | 2854 | </Animated.View> |
| 2750 | ), | 2855 | ), |
| 2751 | [ | 2856 | [ |
| 2752 | copiedRowId, | 2857 | copiedRowId, |
| 2753 | disclosureEnteringEntryIds, | 2858 | disclosureToggleSettling, |
| 2754 | expandedWorkRows, | 2859 | expandedWorkRows, |
| 2755 | workRowSizing, | 2860 | workRowSizing, |
| 2756 | workGroupScrollPositions, | 2861 | workGroupScrollPositions, |
| @@ -2863,6 +2968,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 106 unmodified lines |
| 2863 | data={presentedFeed} | 2968 | data={presentedFeed} |
| 2864 | extraData={listAppearanceData} | 2969 | extraData={listAppearanceData} |
| 2865 | renderItem={renderItem} | 2970 | renderItem={renderItem} |
| | 2971 | viewabilityConfig={THREAD_MEDIA_VIEWABILITY_CONFIG} |
| 2866 | keyExtractor={(entry) => entry.id} | 2972 | keyExtractor={(entry) => entry.id} |
| 2867 | getItemType={(entry) => | 2973 | getItemType={(entry) => |
| 2868 | entry.type === "message" ? `message:${entry.message.role}` : entry.type | 2974 | entry.type === "message" ? `message:${entry.message.role}` : entry.type |
| @@ -2938,4 +3044,5 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { · 69 unmodified lines |
| 2938 | <FilePreviewModal source={expandedFile} onRequestClose={() => setExpandedFile(null)} /> | 3044 | <FilePreviewModal source={expandedFile} onRequestClose={() => setExpandedFile(null)} /> |
| 2939 | </> | 3045 | </> |
| 2940 | ); | 3046 | ); |
| | 3047 | const EMPTY_DISCLOSURE_ENTRY_IDS: ReadonlySet<string> = new Set(); |
| 2941 | }); | 3048 | }); |