Skip to content

Conversation

@alltheseas
Copy link
Collaborator

@alltheseas alltheseas commented Dec 9, 2025

Summary

Adds scroll position persistence so users return to their spot after switching tabs, backgrounding the app, or navigating into threads/profiles. Also persists the "Posts" vs "Posts & Replies" filter selection across app termination.

Scroll Position Persistence:

  • Uses anchor-based tracking (event IDs) rather than pixel offsets, so positions remain valid when new content loads above
  • Positions auto-expire after 24 hours to prevent stale state
  • Enabled for Home and Search timelines
  • Clears saved position when user taps tab bar to scroll to top

Filter Tab Persistence:

  • Changes @SceneStorage to @AppStorage so selection survives app termination

Checklist

Standard PR Checklist

  • I have read (or I am familiar with) the Contribution Guidelines
  • I have tested the changes in this PR
  • I have profiled the changes to ensure there are no performance regressions, or I do not need to profile the changes.
    • If not needed, provide reason: Changes only add lightweight UserDefaults reads/writes on view appear/disappear. No continuous processing or expensive operations.
  • I have opened or referred to an existing github issue related to this change.
  • My PR is either small, or I have split it into smaller logical commits that are easier to review
  • I have added the signoff line to all my commits. See Signing off your work
  • I have added appropriate changelog entries for the changes in this PR. See Adding changelog entries
    • I do not need to add a changelog entry. Reason: [Please provide a reason]
  • I have added appropriate Closes: or Fixes: tags in the commit messages wherever applicable, or made sure those are not needed. See Submitting patches

Test report

Device: Physical iOS device (Xcode compiled)

iOS: Latest

Damus: Commit 5d76ffa (remember-my-spot branch)

Setup: Standard Damus app with logged-in account

Steps:

  1. Scroll down in Home timeline, switch to another tab, switch back
  2. Scroll down in Home timeline, background app, return
  3. Scroll down, tap into a thread, go back
  4. Tap tab bar while in timeline to scroll to top
  5. Select "Posts" tab, kill app, reopen
  6. Scroll in Search timeline, switch tabs, return

Results:

  • PASS

All expected positions are saved and restored correctly.

Closes

Other notes

🤖 Generated with Claude Code

alltheseas and others added 6 commits December 8, 2025 21:14
Introduces anchor-based scroll position tracking that stores event IDs
rather than pixel offsets. This approach handles timeline updates
gracefully - when new posts appear above, the user still returns to
the same content they were viewing.

Key design decisions:
- Uses UserDefaults for simplicity (positions are ~100 bytes each)
- Positions auto-expire after 24 hours to prevent stale state
- Codable for easy serialization

Part of damus-io#3393

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: alltheseas
Adds scrollPositions property to DamusState so all views can access
the shared scroll position manager. Updates all initialization sites:
- DamusState.swift (main init + convenience init + empty state)
- ContentView.swift (app startup)
- TestData.swift (test fixtures)
- MockDamusState.swift (test mocks)

Part of damus-io#3393

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: alltheseas
Implements the core scroll position save/restore mechanism:

InnerTimelineView:
- Adds onEventVisible/onEventHidden callbacks
- Reports when events enter/exit the viewport via onAppear/onDisappear

TimelineView:
- Tracks visible event IDs in a Set
- Adds optional positionKey parameter for enabling persistence
- saveScrollPosition(): finds topmost visible event, saves to manager
- restoreScrollPosition(): scrolls to saved event on view appear
- Clears saved position when user taps "scroll to top"

The anchor-based approach (saving event IDs vs pixel offsets) handles
timeline updates gracefully - new posts above don't affect restoration.

Part of damus-io#3393

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: alltheseas
Passes positionKey to TimelineView in:
- PostingTimelineView: uses .homeTimeline key
- SearchHomeView: uses .search key

This enables automatic save/restore of scroll positions when
users switch between these tabs or background the app.

Part of damus-io#3393

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: alltheseas
Reset hasRestoredPosition flag on disappear so position is
restored when returning from a thread/profile view.

Without this fix, the flag would remain true and block
restoration on subsequent view appearances.

Closes damus-io#3393
Closes damus-io#3395
Closes damus-io#2137
Closes damus-io#2136
Closes damus-io#1720
Closes damus-io#1500

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: alltheseas
Change @SceneStorage to @AppStorage for filter_state so the
user's selection (Posts vs Posts & Replies) survives killing
and reopening the app.

@SceneStorage only persists across scene sessions, not app
termination. @AppStorage uses UserDefaults which persists
across full app restarts.

Closes damus-io#726

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: alltheseas
@danieldaquino danieldaquino added the pr-in-queue This PR is waiting in a queue behind their other PRs marked with the label `pr-active-review`. label Dec 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment