InstantPage V2: support nested <details> blocks

A details block nested inside another rendered (title + chevron) but could not
expand: its body is hosted by a fresh InstantPageV2View whose detailsTapped
closure was never wired (only the top-level view's is, by the bubble), so the
nested title's tap hit a nil closure and was dropped. Wire the body view's
detailsTapped to chain through this view's onTitleTapped, which makeItemView
already routes to the owning view's detailsTapped -> the bubble's toggle
handler; this chains up through arbitrary nesting depth.

Also make the bubble's defaultExpanded(forDetailsIndex:) recurse into expanded
bodies so a nested details whose model default is expanded toggles from the
correct state (the flat top-level scan missed nested indices).

Known pre-existing limitation (unchanged): V2 keys expansion state by a flat
DFS index, so expanding a nested details can shift sibling indices.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
isaac 2026-06-04 15:30:28 +02:00
parent a4cddb74d6
commit 0d5b051820
2 changed files with 22 additions and 4 deletions

View file

@ -1803,6 +1803,13 @@ final class InstantPageV2DetailsView: UIView, InstantPageItemView {
self.addSubview(body)
self.bodyView = body
}
// Forward taps on details NESTED inside this body up to the same toggle handler this
// view uses: makeItemView wired our onTitleTapped to the owning InstantPageV2View's
// detailsTapped, so chaining through onTitleTapped reaches the bubble's toggle handler.
// Without this, a nested details' tap hits the body view's nil detailsTapped and is dropped.
body.detailsTapped = { [weak self] index in
self?.onTitleTapped?(index)
}
body.update(layout: innerLayout, theme: theme, animation: animation)
body.frame = CGRect(
origin: CGPoint(x: 0.0, y: item.titleFrame.maxY),

View file

@ -164,12 +164,23 @@ public class ChatMessageRichDataBubbleContentNode: ChatMessageBubbleContentNode
private func defaultExpanded(forDetailsIndex index: Int) -> Bool {
guard let layout = self.currentPageLayout?.layout else { return false }
for item in layout.items {
if case let .details(d) = item, d.index == index {
return d.defaultExpanded
func search(_ items: [InstantPageV2LaidOutItem]) -> Bool? {
for item in items {
if case let .details(d) = item {
if d.index == index {
return d.defaultExpanded
}
// Recurse into an expanded parent's body so NESTED details indices resolve too;
// the flat top-level scan missed them, leaving the toggle's "current state"
// computation wrong for a nested details whose model default is expanded.
if let inner = d.innerLayout, let found = search(inner.items) {
return found
}
}
}
return nil
}
return false
return search(layout.items) ?? false
}
required public init?(coder aDecoder: NSCoder) {