mirror of
https://github.com/TelegramMessenger/Telegram-iOS.git
synced 2026-07-06 03:33:41 +02:00
ListView: cap pin-to-edge item visible portion at half area
When a pinToEdgeWithInset item is taller than half of the visible area (visibleSize.height - insets.top - insets.bottom), cap its visible portion at halfArea. The remaining height extends past visibleSize - insets.bottom into the bottom-inset region (occluded by overlay UI like the chat input panel). A new private helper `pinToEdgeBottomExtension(forPinnedHeight:)` returns max(0, pinnedHeight - halfArea). Three sites consume it: - calculatePinToEdgeTopInset caps the pinned item's contribution to totalAboveAndPinned via `pinnedHeight - extension`. - replayOperations isPinToEdgeTarget anchors the pinned item's apparent maxY at visibleSize - insets.bottom + extension. - isStrictlyScrolledToPinToEdgeItem matches the new anchor. Both isPinToEdgeTarget and isStrictlyScrolledToPinToEdgeItem fire when either calculatePinToEdgeTopInset() > 0 OR extension > 0, so the cap also applies when items above the pinned item overflow the visible area (in which case calculatePinToEdgeTopInset returns 0 but extension is still positive). When the pinned item fits within halfArea, extension == 0 and behavior is identical to before this change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f25d0776c7
commit
999aac6eb3
3 changed files with 32 additions and 402 deletions
|
|
@ -1,262 +0,0 @@
|
|||
# ListView pin-to-edge half-area cap — implementation plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Cap the visible portion of a `pinToEdgeWithInset` item in `ListViewImpl` at half the visible area, so a tall pinned item can't take over the whole screen.
|
||||
|
||||
**Architecture:** Add a single private helper on `ListViewImpl` that returns the *bottom extension* (`max(0, pinnedHeight − halfArea)`). Three existing sites in `ListView.swift` consume the extension to (1) cap the pinned-item contribution to `pinToEdgeTopInset`, (2) lower the pin-to-edge-target scroll anchor, and (3) match the new anchor in `isStrictlyScrolledToPinToEdgeItem`. When `extension == 0`, all three sites compute exactly what they compute today.
|
||||
|
||||
**Tech Stack:** Swift, Bazel build via `Make.py`. No tests in this project (per CLAUDE.md) — verification is full build + manual smoke.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-05-01-listview-pin-to-edge-half-cap-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
All edits in one file: `submodules/Display/Source/ListView.swift`.
|
||||
|
||||
| Site | Lines (current) | Change |
|
||||
|------|-----------------|--------|
|
||||
| Helper (new) | inserted right after `calculatePinToEdgeTopInset` | new private method `pinToEdgeBottomExtension(forPinnedHeight:)` |
|
||||
| `calculatePinToEdgeTopInset` | 1094–1119 | cap the pinned item's contribution to `totalAboveAndPinned` at `halfArea` |
|
||||
| `isStrictlyScrolledToPinToEdgeItem` | 2683 | use `extension` to compute `expectedMaxY` |
|
||||
| pin-to-edge-target offset | 3127 | use `extension` to compute the target offset |
|
||||
|
||||
All four edits must land in a single commit — committing one without the others leaves the inset calculation and the actual anchor inconsistent.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Apply the four edits to `ListView.swift`
|
||||
|
||||
**Files:**
|
||||
- Modify: `submodules/Display/Source/ListView.swift` (lines 1094–1119, 2683, 3127, plus new helper)
|
||||
|
||||
- [ ] **Step 1: Update `calculatePinToEdgeTopInset()` to cap the pinned item's contribution**
|
||||
|
||||
Find the existing function (currently lines 1094–1119):
|
||||
|
||||
```swift
|
||||
private func calculatePinToEdgeTopInset() -> CGFloat {
|
||||
var lowestPinnedIndex: Int = Int.max
|
||||
for itemNode in self.itemNodes {
|
||||
guard let index = itemNode.index, index >= 0, index < self.items.count else { continue }
|
||||
if index < lowestPinnedIndex && self.items[index].pinToEdgeWithInset {
|
||||
lowestPinnedIndex = index
|
||||
}
|
||||
}
|
||||
guard lowestPinnedIndex != Int.max else { return 0.0 }
|
||||
|
||||
var totalAboveAndPinned: CGFloat = 0.0
|
||||
var sawIndexZero = false
|
||||
for itemNode in self.itemNodes {
|
||||
guard let index = itemNode.index else { continue }
|
||||
if index == 0 {
|
||||
sawIndexZero = true
|
||||
}
|
||||
if index <= lowestPinnedIndex {
|
||||
totalAboveAndPinned += itemNode.apparentBounds.height
|
||||
}
|
||||
}
|
||||
guard sawIndexZero else { return 0.0 }
|
||||
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
return max(0.0, visibleArea - totalAboveAndPinned)
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
private func calculatePinToEdgeTopInset() -> CGFloat {
|
||||
var lowestPinnedIndex: Int = Int.max
|
||||
for itemNode in self.itemNodes {
|
||||
guard let index = itemNode.index, index >= 0, index < self.items.count else { continue }
|
||||
if index < lowestPinnedIndex && self.items[index].pinToEdgeWithInset {
|
||||
lowestPinnedIndex = index
|
||||
}
|
||||
}
|
||||
guard lowestPinnedIndex != Int.max else { return 0.0 }
|
||||
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
let halfArea = visibleArea * 0.5
|
||||
|
||||
var totalAboveAndPinned: CGFloat = 0.0
|
||||
var sawIndexZero = false
|
||||
for itemNode in self.itemNodes {
|
||||
guard let index = itemNode.index else { continue }
|
||||
if index == 0 {
|
||||
sawIndexZero = true
|
||||
}
|
||||
if index < lowestPinnedIndex {
|
||||
totalAboveAndPinned += itemNode.apparentBounds.height
|
||||
} else if index == lowestPinnedIndex {
|
||||
let pinnedHeight = itemNode.apparentBounds.height
|
||||
let effectivePinnedHeight = halfArea > 0.0 ? min(pinnedHeight, halfArea) : pinnedHeight
|
||||
totalAboveAndPinned += effectivePinnedHeight
|
||||
}
|
||||
}
|
||||
guard sawIndexZero else { return 0.0 }
|
||||
|
||||
return max(0.0, visibleArea - totalAboveAndPinned)
|
||||
}
|
||||
|
||||
private func pinToEdgeBottomExtension(forPinnedHeight pinnedHeight: CGFloat) -> CGFloat {
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
let halfArea = visibleArea * 0.5
|
||||
guard halfArea > 0.0 else { return 0.0 }
|
||||
return max(0.0, pinnedHeight - halfArea)
|
||||
}
|
||||
```
|
||||
|
||||
Key changes:
|
||||
- `let visibleArea` and `let halfArea` moved up so they're available inside the loop.
|
||||
- The `if index <= lowestPinnedIndex` branch is split into `< lowestPinnedIndex` (full height) and `== lowestPinnedIndex` (capped height).
|
||||
- New helper `pinToEdgeBottomExtension(forPinnedHeight:)` added immediately after.
|
||||
|
||||
- [ ] **Step 2: Update `isStrictlyScrolledToPinToEdgeItem()` to use the extension**
|
||||
|
||||
Find (currently around line 2683):
|
||||
|
||||
```swift
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.index == targetIndex {
|
||||
let expectedMaxY = (self.visibleSize.height - self.insets.bottom) + itemNode.scrollPositioningInsets.bottom
|
||||
return abs(itemNode.apparentFrame.maxY - expectedMaxY) < 0.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.index == targetIndex {
|
||||
let extensionOffset = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
let expectedMaxY = (self.visibleSize.height - self.insets.bottom + extensionOffset) + itemNode.scrollPositioningInsets.bottom
|
||||
return abs(itemNode.apparentFrame.maxY - expectedMaxY) < 0.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update the pin-to-edge-target scroll offset in `replayOperations`**
|
||||
|
||||
Find (currently around line 3127, inside `if isPinToEdgeTarget {`):
|
||||
|
||||
```swift
|
||||
var offset: CGFloat
|
||||
if isPinToEdgeTarget {
|
||||
offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom
|
||||
} else {
|
||||
```
|
||||
|
||||
Replace with:
|
||||
|
||||
```swift
|
||||
var offset: CGFloat
|
||||
if isPinToEdgeTarget {
|
||||
let extensionOffset = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
offset = (self.visibleSize.height - insets.bottom + extensionOffset) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom
|
||||
} else {
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Sanity-grep for any other use of the old anchor formula**
|
||||
|
||||
Run from repo root:
|
||||
|
||||
```bash
|
||||
grep -nE "visibleSize\.height\s*-\s*insets?\.bottom\s*\)" submodules/Display/Source/ListView.swift
|
||||
```
|
||||
|
||||
Inspect each hit. Sites listed in this plan (now patched) and `else` branches (`.bottom(additionalOffset)`, `.top(additionalOffset)`, `.center`, `.visible` at lines 3131, 3143, 3146, 3154, 3160) are scroll-position offsets for *non-pinned* items — they must NOT change. Confirm the only patched lines are the pin-to-edge-target paths.
|
||||
|
||||
- [ ] **Step 5: Run the full Bazel build with `--continueOnError`**
|
||||
|
||||
```bash
|
||||
source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \
|
||||
--cacheDir ~/telegram-bazel-cache build \
|
||||
--configurationPath build-system/appstore-configuration.json \
|
||||
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
|
||||
--gitCodesigningType development --gitCodesigningUseCurrent \
|
||||
--buildNumber=1 --configuration=debug_sim_arm64 --continueOnError
|
||||
```
|
||||
|
||||
Expected: build completes successfully (no Swift errors). If the build surfaces errors in `submodules/Display/Source/ListView.swift`, re-read the patched sections against Step 1–3 above and fix.
|
||||
|
||||
If unrelated errors appear in untouched files, they are pre-existing — note them and continue.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add submodules/Display/Source/ListView.swift
|
||||
git commit -m "$(cat <<'EOF'
|
||||
ListView: cap pin-to-edge item visible portion at half area
|
||||
|
||||
When a pinToEdgeWithInset item is taller than half of the visible
|
||||
area, its visible portion is now capped at halfArea. The remaining
|
||||
height extends below visibleSize - insets.bottom into the
|
||||
bottom-inset region, where it is occluded by overlay UI (input
|
||||
panel, tab bar) in typical usage.
|
||||
|
||||
Three sites updated to a single helper pinToEdgeBottomExtension:
|
||||
calculatePinToEdgeTopInset (caps the pinned item's contribution),
|
||||
the pin-to-edge-target scroll offset in replayOperations, and
|
||||
isStrictlyScrolledToPinToEdgeItem.
|
||||
|
||||
When the pinned item fits within halfArea, extension == 0 and all
|
||||
three sites compute exactly what they did before.
|
||||
|
||||
Spec: docs/superpowers/specs/2026-05-01-listview-pin-to-edge-half-cap-design.md
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Manual smoke test
|
||||
|
||||
The change must be exercised on a real device/simulator because there are no unit tests for ListView in this project.
|
||||
|
||||
**Test bench:** any chat that supports `pinToTop` messages (the only consumer of `pinToEdgeWithInset == true` per `ChatMessageItemImpl.swift:280`).
|
||||
|
||||
- [ ] **Step 1: Short pinned message — unchanged behavior**
|
||||
|
||||
Open a chat with a `pinToTop` message of ~1–3 lines (well under `halfArea`). Confirm:
|
||||
- Pinned message is anchored at the top of the visible chat area (top of screen, given rotated chat).
|
||||
- The list scrolls under it as expected.
|
||||
- No visible difference from the pre-change build.
|
||||
|
||||
- [ ] **Step 2: Mid-height pinned message — unchanged behavior**
|
||||
|
||||
Open a chat with a `pinToTop` message that is sized just under `halfArea` (roughly 30–40% of the chat area). Confirm same as Step 1 — no behavior change.
|
||||
|
||||
- [ ] **Step 3: Tall pinned message — capped behavior**
|
||||
|
||||
Open a chat with a `pinToTop` message that is much taller than `halfArea` (e.g., a long text message that would naturally span more than half the visible chat area). Confirm:
|
||||
- The pinned message occupies at most ~half of the visible chat area at the top of the screen.
|
||||
- The remaining (lower) half of the chat area shows the regular thread content.
|
||||
- The portion of the pinned message that "doesn't fit" is not visible — it should be occluded by the input panel / nav bar overlay.
|
||||
- Scrolling behaves consistently: the pinned message stays anchored at the top edge, the rest of the thread scrolls underneath.
|
||||
|
||||
- [ ] **Step 4: Inset / size changes — re-evaluation**
|
||||
|
||||
While viewing a chat with a tall pinned message, open and close the keyboard (this changes `insets.bottom`). The cap should re-evaluate on each layout pass — no jumpy or stuck state. The pinned message's visible portion should remain ~half of the new visible area.
|
||||
|
||||
If any of Steps 1–4 fails, do NOT call the work done. Re-open the spec, re-read the patched sites in `ListView.swift`, and identify which assumption broke.
|
||||
|
||||
---
|
||||
|
||||
## Self-review
|
||||
|
||||
**Spec coverage:**
|
||||
- Helper `pinToEdgeBottomExtension(forPinnedHeight:)` → Task 1 Step 1.
|
||||
- Cap in `calculatePinToEdgeTopInset` → Task 1 Step 1.
|
||||
- Anchor change in pin-to-edge-target offset → Task 1 Step 3.
|
||||
- Anchor change in `isStrictlyScrolledToPinToEdgeItem` → Task 1 Step 2.
|
||||
- Edge cases (`pinnedHeight ≤ halfArea`, multiple pinned items, degenerate inset, dynamic resize, rotated chats) — covered by the helper's `max(0, …)` and the unchanged `lowestPinnedIndex` selection logic; smoke-tested in Task 2 Steps 1, 2, 4.
|
||||
- Verification (full build, manual smoke) → Task 1 Step 5, Task 2.
|
||||
- Risks (overflow into bottom-inset region) → Task 2 Step 3 explicitly checks occlusion.
|
||||
|
||||
**Type/name consistency:** Helper signature `pinToEdgeBottomExtension(forPinnedHeight pinnedHeight: CGFloat) -> CGFloat` is identical at the declaration (Step 1) and both call sites (Steps 2, 3). Local variable name `extensionOffset` matches in Steps 2 and 3.
|
||||
|
||||
**Placeholder scan:** No TBDs, no "implement appropriately", no missing code blocks. Each step's expected commands and outcomes are concrete.
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
# ListView pin-to-edge half-area cap — design
|
||||
|
||||
## Problem
|
||||
|
||||
In `ListViewImpl` (`submodules/Display/Source/ListView.swift`), an item that opts into `pinToEdgeWithInset` is anchored with its `apparentFrame.maxY` at `visibleSize.height − insets.bottom`. When the pinned item is taller than the available area, it dominates the entire visible region — its top extends above the top of the visible area, hiding adjacent content.
|
||||
|
||||
The intended UX is that a pinned item never occupies more than half of the visible area, so context above (in list coords) / below (visually, in rotated chats) the pinned item remains visible.
|
||||
|
||||
## Goal
|
||||
|
||||
When the pinned item's height exceeds `halfArea = (visibleSize.height − insets.top − insets.bottom) / 2`, cap the pinned item's *visible portion* at `halfArea`. The remaining `pinnedHeight − halfArea` extends below the content area (between `visibleSize − insets.bottom` and `visibleSize`), into the bottom-inset region. The list view has `clipsToBounds = true` (line 498), so anything beyond `visibleSize.height` is clipped; in typical usage the bottom-inset region is occluded by overlay UI (chat input panel, tab bar) drawn on top of the list view.
|
||||
|
||||
When the pinned item's height is `≤ halfArea`, behavior is identical to the current implementation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No new public API on `ListView` or `ListViewItem`. The cap fraction is `0.5`, hard-coded.
|
||||
- No changes to consumers (`ChatMessageItemImpl`, etc.).
|
||||
- No changes to header/accessory layout.
|
||||
|
||||
## Approach
|
||||
|
||||
A single private helper computes the *bottom extension* — the distance by which the pinned item's bottom edge is pushed below `visibleSize − bottom`:
|
||||
|
||||
```
|
||||
extension = max(0, pinnedHeight − halfArea)
|
||||
```
|
||||
|
||||
When `extension > 0`, the pinned item's `apparentFrame.maxY` is anchored at `visibleSize − bottom + extension`, so its top edge sits at `visibleSize − bottom − halfArea`. The visible portion is exactly `halfArea`.
|
||||
|
||||
Three existing sites in `ListView.swift` need to agree on the new anchor; all three use the same helper.
|
||||
|
||||
### Helper
|
||||
|
||||
Add to `ListViewImpl`:
|
||||
|
||||
```swift
|
||||
private func pinToEdgeBottomExtension(forPinnedHeight pinnedHeight: CGFloat) -> CGFloat {
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
let halfArea = visibleArea * 0.5
|
||||
guard halfArea > 0.0 else { return 0.0 }
|
||||
return max(0.0, pinnedHeight - halfArea)
|
||||
}
|
||||
```
|
||||
|
||||
### Call site 1 — `calculatePinToEdgeTopInset()` (around line 1094)
|
||||
|
||||
The pinned item's contribution to `totalAboveAndPinned` is capped at `halfArea`. This grows `pinToEdgeTopInset` when the pinned item is too tall, pushing items-above further down so their stack lines up with the new (lowered) pinned-item top edge.
|
||||
|
||||
```swift
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
let halfArea = visibleArea * 0.5
|
||||
|
||||
var totalAboveAndPinned: CGFloat = 0.0
|
||||
var sawIndexZero = false
|
||||
for itemNode in self.itemNodes {
|
||||
guard let index = itemNode.index else { continue }
|
||||
if index == 0 { sawIndexZero = true }
|
||||
if index < lowestPinnedIndex {
|
||||
totalAboveAndPinned += itemNode.apparentBounds.height
|
||||
} else if index == lowestPinnedIndex {
|
||||
let pinnedHeight = itemNode.apparentBounds.height
|
||||
let effectivePinnedHeight = halfArea > 0.0 ? min(pinnedHeight, halfArea) : pinnedHeight
|
||||
totalAboveAndPinned += effectivePinnedHeight
|
||||
}
|
||||
}
|
||||
guard sawIndexZero else { return 0.0 }
|
||||
return max(0.0, visibleArea - totalAboveAndPinned)
|
||||
```
|
||||
|
||||
### Call site 2 — pin-to-edge-target scroll offset (around line 3127)
|
||||
|
||||
```swift
|
||||
if isPinToEdgeTarget {
|
||||
let extensionOffset = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
offset = (self.visibleSize.height - insets.bottom + extensionOffset) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom
|
||||
}
|
||||
```
|
||||
|
||||
### Call site 3 — `isStrictlyScrolledToPinToEdgeItem()` (around line 2683)
|
||||
|
||||
```swift
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.index == targetIndex {
|
||||
let extensionOffset = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
let expectedMaxY = (self.visibleSize.height - self.insets.bottom + extensionOffset) + itemNode.scrollPositioningInsets.bottom
|
||||
return abs(itemNode.apparentFrame.maxY - expectedMaxY) < 0.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All three sites read `apparentBounds.height` for consistency with the rest of the file (which uses post-layout, post-animation heights for pinning math).
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **`pinnedHeight ≤ halfArea`**: helper returns 0, all three sites compute identical values to the current implementation. No behavioural change in the common case.
|
||||
- **Multiple items with `pinToEdgeWithInset == true`**: existing semantics select the smallest-indexed one as `lowestPinnedIndex`. The cap applies only to that item. Others sit above as normal items.
|
||||
- **`visibleSize.height ≤ insets.top + insets.bottom`** (degenerate / unmeasured layout): `halfArea ≤ 0`, helper returns 0, no cap.
|
||||
- **Inset / size changes**: `pinToEdgeTopInset` and the scroll offset are recomputed on every `snapToBounds` / `replayOperations`, so the cap re-evaluates whenever inputs change.
|
||||
- **Rotated chats (the actual usage path)**: list-coord-bottom maps to top-of-screen; capping the pinned item's visible portion at `halfArea` in list coords keeps the start of the pinned message visible at the top of the screen with the rest extending above.
|
||||
|
||||
## Verification
|
||||
|
||||
No unit tests exist for ListView in this project (per CLAUDE.md). Verification is:
|
||||
|
||||
1. **Full build** with `--continueOnError`:
|
||||
```
|
||||
source ~/.zshrc 2>/dev/null; python3 build-system/Make/Make.py --overrideXcodeVersion \
|
||||
--cacheDir ~/telegram-bazel-cache build \
|
||||
--configurationPath build-system/appstore-configuration.json \
|
||||
--gitCodesigningRepository git@gitlab.com:peter-iakovlev/fastlanematch.git \
|
||||
--gitCodesigningType development --gitCodesigningUseCurrent \
|
||||
--buildNumber=1 --configuration=debug_sim_arm64 --continueOnError
|
||||
```
|
||||
2. **Manual smoke test** in a chat with a pinned message:
|
||||
- Short pinned message (`< halfArea`): unchanged behavior — pinned message anchored at top of screen, list scrolls under it.
|
||||
- Mid-height pinned message (just under `halfArea`): unchanged.
|
||||
- Tall pinned message (much taller than `halfArea`): only ~half of the visible area shows the pinned message; the other half remains available for the chat thread.
|
||||
|
||||
## Risks
|
||||
|
||||
- The pinned item's bottom extends past `visibleSize.height − insets.bottom` when capped, into the bottom-inset region of the list view's bounds. In contexts where the bottom-inset region is *not* covered by overlay UI (e.g., a list with `insets.bottom = 0`, or a list with bottom inset reserved for spacing rather than for an overlay), the overflow tail of the pinned item is briefly visible above the bottom edge of the list view. For the primary consumer (chat with `pinToTop` messages), the input panel is drawn on top and occludes the region. Confirm during manual smoke test on non-chat consumers if any.
|
||||
- The cap is half hard-coded. If a future feature wants per-item or per-list configuration, this becomes a public knob; for now keep it private.
|
||||
|
|
@ -1101,6 +1101,8 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
|
|||
}
|
||||
guard lowestPinnedIndex != Int.max else { return 0.0 }
|
||||
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
|
||||
var totalAboveAndPinned: CGFloat = 0.0
|
||||
var sawIndexZero = false
|
||||
for itemNode in self.itemNodes {
|
||||
|
|
@ -1108,16 +1110,25 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
|
|||
if index == 0 {
|
||||
sawIndexZero = true
|
||||
}
|
||||
if index <= lowestPinnedIndex {
|
||||
if index < lowestPinnedIndex {
|
||||
totalAboveAndPinned += itemNode.apparentBounds.height
|
||||
} else if index == lowestPinnedIndex {
|
||||
let pinnedHeight = itemNode.apparentBounds.height
|
||||
totalAboveAndPinned += pinnedHeight - self.pinToEdgeBottomExtension(forPinnedHeight: pinnedHeight)
|
||||
}
|
||||
}
|
||||
guard sawIndexZero else { return 0.0 }
|
||||
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
return max(0.0, visibleArea - totalAboveAndPinned)
|
||||
}
|
||||
|
||||
private func pinToEdgeBottomExtension(forPinnedHeight pinnedHeight: CGFloat) -> CGFloat {
|
||||
let visibleArea = self.visibleSize.height - self.insets.top - self.insets.bottom
|
||||
let halfArea = visibleArea * 0.5
|
||||
guard halfArea > 0.0 else { return 0.0 }
|
||||
return max(0.0, pinnedHeight - halfArea)
|
||||
}
|
||||
|
||||
private func areAllItemsOnScreen() -> Bool {
|
||||
if self.itemNodes.count == 0 {
|
||||
return true
|
||||
|
|
@ -2672,15 +2683,17 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
|
|||
}
|
||||
|
||||
public func isStrictlyScrolledToPinToEdgeItem() -> Bool {
|
||||
if self.calculatePinToEdgeTopInset() <= 0.0 {
|
||||
return false
|
||||
}
|
||||
guard let targetIndex = self.items.firstIndex(where: { $0.pinToEdgeWithInset }) else {
|
||||
return false
|
||||
}
|
||||
let pinToEdgeTopInset = self.calculatePinToEdgeTopInset()
|
||||
for itemNode in self.itemNodes {
|
||||
if itemNode.index == targetIndex {
|
||||
let expectedMaxY = (self.visibleSize.height - self.insets.bottom) + itemNode.scrollPositioningInsets.bottom
|
||||
let extensionOffset = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
guard pinToEdgeTopInset > 0.0 || extensionOffset > 0.0 else {
|
||||
return false
|
||||
}
|
||||
let expectedMaxY = (self.visibleSize.height - self.insets.bottom + extensionOffset) + itemNode.scrollPositioningInsets.bottom
|
||||
return abs(itemNode.apparentFrame.maxY - expectedMaxY) < 0.5
|
||||
}
|
||||
}
|
||||
|
|
@ -3108,23 +3121,25 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
|
|||
let insets = self.insets// updateSizeAndInsets?.insets ?? self.insets
|
||||
|
||||
var isPinToEdgeTarget = false
|
||||
if self.calculatePinToEdgeTopInset() > 0.0,
|
||||
index >= 0, index < self.items.count,
|
||||
self.items[index].pinToEdgeWithInset {
|
||||
isPinToEdgeTarget = true
|
||||
for otherNode in self.itemNodes {
|
||||
guard let otherIndex = otherNode.index else { continue }
|
||||
guard otherIndex >= 0, otherIndex < self.items.count else { continue }
|
||||
if otherIndex < index, self.items[otherIndex].pinToEdgeWithInset {
|
||||
isPinToEdgeTarget = false
|
||||
break
|
||||
if index >= 0, index < self.items.count, self.items[index].pinToEdgeWithInset {
|
||||
let pinExtension = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
if self.calculatePinToEdgeTopInset() > 0.0 || pinExtension > 0.0 {
|
||||
isPinToEdgeTarget = true
|
||||
for otherNode in self.itemNodes {
|
||||
guard let otherIndex = otherNode.index else { continue }
|
||||
guard otherIndex >= 0, otherIndex < self.items.count else { continue }
|
||||
if otherIndex < index, self.items[otherIndex].pinToEdgeWithInset {
|
||||
isPinToEdgeTarget = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var offset: CGFloat
|
||||
if isPinToEdgeTarget {
|
||||
offset = (self.visibleSize.height - insets.bottom) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom
|
||||
let extensionOffset = self.pinToEdgeBottomExtension(forPinnedHeight: itemNode.apparentBounds.height)
|
||||
offset = (self.visibleSize.height - insets.bottom + extensionOffset) - itemNode.apparentFrame.maxY + itemNode.scrollPositioningInsets.bottom
|
||||
} else {
|
||||
switch scrollToItem.position {
|
||||
case let .bottom(additionalOffset):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue