Skip to content

fix: preserve search scroll and recover blank virtual grid - #658

Merged
hmjn023 merged 11 commits into
developfrom
fix/search-scroll-and-grid-restore
Aug 6, 2026
Merged

fix: preserve search scroll and recover blank virtual grid#658
hmjn023 merged 11 commits into
developfrom
fix/search-scroll-and-grid-restore

Conversation

@hmjn023

@hmjn023 hmjn023 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

概要

取り込み済みのPR #657後に確認された、検索画面のスクロール未保存とsource一覧の復帰後のグリッド白画面を修正します。

変更内容

  • 検索条件の復元時に現在のsearchState.scrollYを保持
  • vector検索の状態復元でもスクロール位置を保持
  • 仮想グリッドの測定範囲が一時的に未確定でも、マウント済み画像のロードを停止しない
  • 検索スクロール保持の回帰テストを追加

検証

  • packages/ui tests: 58 passed
  • packages/ui typecheck
  • apps/server typecheck
  • 仮想グリッドE2E: 4 passed

Summary by CodeRabbit

  • バグ修正
    • 検索状態やプリセット、ベクトル検索モードの復元・適用時に、現在のスクロール位置が維持されるようになりました。
    • スクロール復元を中断した場合も、選択した位置が正しく保存されるようになりました。
    • 仮想スクロール中や深いスクロール位置の復元後も、画像が継続して正常に読み込まれるようになりました。
    • ページ追加や画面の再マウント後も、スクロール位置と画像の読み込み状態が維持されるようになりました。
    • 検索・メディア一覧のエラーや読み込み状態が、スクロールに影響されにくい固定領域に表示されるようになりました。
    • データ取得中も既に表示された検索結果が維持され、総件数が正しく表示されるようになりました。

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

検索結果のキャッシュ保持、scrollY の復元、ギャラリーの追加取得、仮想グリッドの画像読み込みを更新します。V2画面ではスクロールコンテナを識別値で指定します。

Changes

検索結果とメディア結果の保持

Layer / File(s) Summary
検索結果とメディア結果の保持
packages/ui/src/hooks/use-search-page.ts, packages/ui/src/hooks/use-source-media-page.ts, packages/ui/src/screens/*
クエリ結果を保持するシグナルを追加します。画面は保持データから総件数とデータ有無を取得します。取得中も保持済み結果を参照します。

検索状態とスクロール位置の復元

Layer / File(s) Summary
検索状態とスクロール位置の復元
packages/ui/src/hooks/use-current-search-persistence.ts, packages/ui/src/hooks/use-current-search-persistence.test.ts, packages/ui/src/hooks/scroll-container.ts
プリセットおよびベクターモードの復元時に既存のscrollYを保持します。復元中のユーザー操作を処理し、仮想化コンテナへスクロールイベントを再通知します。

ギャラリーの復元と追加取得

Layer / File(s) Summary
ギャラリーの復元と追加取得
apps/server/src/tests/e2e/ui-gallery/src.tsx, apps/server/src/tests/e2e/ui-components.gallery.spec.ts
復元・再マウント時の初期件数と追加取得を管理します。スクロール位置、取得状態、グリッドの再マウントを検証します。

仮想グリッドとスクロール対象の更新

Layer / File(s) Summary
仮想グリッドとスクロール対象の更新
packages/ui/src/source-media-grid.tsx, packages/ui/src/screens/v2-*.tsx, packages/ui/src/source-media-page.tsx, apps/server/src/routes/v2/search.tsx
測定範囲がない場合も画像を有効化し、loading: "eager"を使用します。仮想化範囲の再測定後にスクロール位置を通知します。V2のスクロール領域に識別値を設定し、状態表示を上部へ移動します。

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VirtualGridGallery
  participant useScrollRestoration
  participant SourceMediaGrid
  participant Browser
  VirtualGridGallery->>useScrollRestoration: 復元位置と追加取得状態を渡す
  useScrollRestoration->>VirtualGridGallery: 追加取得処理を実行する
  VirtualGridGallery->>SourceMediaGrid: ページ結果と取得状態を渡す
  SourceMediaGrid->>Browser: 画像をeager loadingで有効化する
  Browser-->>SourceMediaGrid: 画像読み込み状態を返す
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは検索スクロールの保持と空の仮想グリッドの復旧という主要変更を明確かつ簡潔に示しています。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/search-scroll-and-grid-restore

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/ui/src/hooks/use-current-search-persistence.test.ts (1)

188-200: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

ベクターモードの scrollY 保持をテストしてください。

現在の mode: "vector" のテストは legacy の selectedSource 分岐だけを検証しています。mode: "vector"searchState.scrollY を設定してから scrollY が保持されることを単体テストで追加してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/hooks/use-current-search-persistence.test.ts` around lines
188 - 200, Extend the current scroll-position persistence test coverage for
vector mode by configuring the test state with mode: "vector" and setting
searchState.scrollY before restoring the persisted search state. Assert that the
vector-mode restore path preserves the same scrollY value, alongside the
existing legacy selectedSource coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/ui/src/hooks/use-current-search-persistence.test.ts`:
- Around line 188-200: Extend the current scroll-position persistence test
coverage for vector mode by configuring the test state with mode: "vector" and
setting searchState.scrollY before restoring the persisted search state. Assert
that the vector-mode restore path preserves the same scrollY value, alongside
the existing legacy selectedSource coverage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c113cb3-6825-4ffb-9c65-a78214979ded

📥 Commits

Reviewing files that changed from the base of the PR and between 16665b2 and 1fc0604.

📒 Files selected for processing (3)
  • packages/ui/src/hooks/use-current-search-persistence.test.ts
  • packages/ui/src/hooks/use-current-search-persistence.ts
  • packages/ui/src/source-media-grid.tsx

hmjn023 added 2 commits August 2, 2026 22:30
Keep mounted virtual rows loadable while navigation restoration settles and persist user-owned scroll positions when restoration is cancelled. Add a paginated deep-scroll regression fixture covering post-restore scrolling.
Re-measure the nested virtualizer when its scroll container and content offset become available, and eagerly load mounted rows while the initial range is settling.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/tests/e2e/ui-components.gallery.spec.ts`:
- Around line 268-270: Strengthen the scroll restoration assertion in the
scroller polling block by requiring scrollTop to reach a lower bound close to
the intended 20,000 px restoration target, rather than merely exceeding 1,000.
Keep the existing polling behavior and ensure the threshold still allows only a
near-target restored position to pass.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2ff29b0c-ce75-4421-b57e-db3f1407ee5c

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc0604 and a965cd3.

📒 Files selected for processing (4)
  • apps/server/src/tests/e2e/ui-components.gallery.spec.ts
  • apps/server/src/tests/e2e/ui-gallery/src.tsx
  • packages/ui/src/hooks/scroll-container.ts
  • packages/ui/src/source-media-grid.tsx

Comment thread apps/server/src/tests/e2e/ui-components.gallery.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ui/src/source-media-page.tsx`:
- Around line 125-128: Update the scrollContainerSelector assignment in
V2SourceMediaScreen to apply the same fallback as the rendered data-media-scroll
attribute: use props.mediaSourceId() with "v2-source" when undefined, so the
selector targets the actual scroll container.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 832cb305-cbf7-494d-b7c7-3ceba062e54a

📥 Commits

Reviewing files that changed from the base of the PR and between a965cd3 and dc3925f.

📒 Files selected for processing (4)
  • apps/server/src/routes/v2/search.tsx
  • packages/ui/src/screens/v2-search-screen.tsx
  • packages/ui/src/screens/v2-source-media-screen.tsx
  • packages/ui/src/source-media-page.tsx

Comment thread packages/ui/src/source-media-page.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/tests/e2e/ui-components.gallery.spec.ts`:
- Around line 279-329: Strengthen the final scroll-position assertion in the
test “virtual media grid preserves offset after a route-style remount and page
update” to validate that scrollTop reaches a value close to the computed nextTop
target, rather than only exceeding restoredTop by 100. Keep the existing append
and image-loading checks unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 790e44e8-c65e-4405-afdb-5e0ee172ee24

📥 Commits

Reviewing files that changed from the base of the PR and between dc3925f and 53c7204.

📒 Files selected for processing (4)
  • apps/server/src/tests/e2e/ui-components.gallery.spec.ts
  • apps/server/src/tests/e2e/ui-gallery/src.tsx
  • packages/ui/src/hooks/scroll-container.ts
  • packages/ui/src/source-media-grid.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/ui/src/source-media-grid.tsx
  • apps/server/src/tests/e2e/ui-gallery/src.tsx

Comment thread apps/server/src/tests/e2e/ui-components.gallery.spec.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/ui/src/hooks/use-search-page.ts (1)

1-1: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

dataUpdatedAt への裸のアクセスに void を付けてください。 両ファイルの新しいキャッシュ同期用 createEffect は、リアクティブな依存関係を張るためだけに dataUpdatedAt を参照しますが、戻り値を使わない式文のままです。use-source-media-page.ts の347行目にある既存の void hasNext; と同じ表記に揃えてください。

  • packages/ui/src/hooks/use-search-page.ts#L184-204: searchResultQuery.dataUpdatedAt;void searchResultQuery.dataUpdatedAt; に変更してください。
  • packages/ui/src/hooks/use-source-media-page.ts#L272-289: mediaQuery.dataUpdatedAt;void mediaQuery.dataUpdatedAt; に変更してください。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/hooks/use-search-page.ts` at line 1, Update the
cache-synchronization createEffect blocks in use-search-page.ts and
use-source-media-page.ts to prefix the unused reactive accesses
searchResultQuery.dataUpdatedAt and mediaQuery.dataUpdatedAt with void, matching
the existing void hasNext style while preserving their reactive dependency
behavior.

Source: Coding guidelines

🧹 Nitpick comments (3)
packages/ui/src/hooks/use-search-page.ts (2)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

キャッシュ同期とデデュープのロジックが2ファイルで重複しています。 どちらのフックも、queryClient.getQueryData によるキャッシュ読み取り、InfiniteData シグナルへの同期、Set を使った重複除去、contentState の組み立てまで、ほぼ同一の実装です。共有フック(例: useCachedInfiniteQueryResult)への抽出を検討してください。

  • packages/ui/src/hooks/use-search-page.ts#L184-227: searchResultQueryOptions/searchResultQuery/searchResultData 周りのキャッシュ同期・デデュープ・contentState 生成ロジックを共有ヘルパーへ切り出してください。
  • packages/ui/src/hooks/use-source-media-page.ts#L260-313: mediaQueryOptions/mediaQuery/mediaQueryData 周りの同一ロジックを同じ共有ヘルパーに置き換えてください。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/hooks/use-search-page.ts` at line 1, Extract the duplicated
cache synchronization, InfiniteData signal updates, Set-based deduplication, and
contentState construction from use-search-page.ts and use-source-media-page.ts
into a shared hook such as useCachedInfiniteQueryResult. Update the
searchResultQuery/searchResultData and mediaQuery/mediaQueryData flows to use
that helper while preserving their existing query options and resulting content
state behavior.

184-227: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

キャッシュ同期処理が use-source-media-page.ts と重複しています。

この効果とデデュープ処理(184-227行)は、use-source-media-page.ts の272-313行とほぼ同一の実装です。共通ロジックを共有フックへ抽出することを検討してください。詳細は本レビューの consolidated_comments セクションを参照してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/hooks/use-search-page.ts` around lines 184 - 227, Extract the
duplicated cache-synchronization effect and page-result deduplication currently
used by searchResults/contentState into a shared hook or utility, then reuse it
from both use-search-page and use-source-media-page. Preserve the existing
query-key cache updates, placeholder handling, deduplication by media id, and UI
state behavior while removing the duplicated implementations.
packages/ui/src/hooks/use-source-media-page.ts (1)

260-313: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

キャッシュ同期処理が use-search-page.ts と重複しています。

この効果とデデュープ処理(260-313行)は、use-search-page.ts の184-227行とほぼ同一の実装です。詳細は本レビューの consolidated_comments セクションを参照してください。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/hooks/use-source-media-page.ts` around lines 260 - 313,
Remove the duplicated cache-synchronization and result-deduplication logic from
the media page flow around mediaQueryData, the createEffect, mediaResults, and
contentState. Reuse the existing shared implementation or abstraction
established by use-search-page.ts, preserving the current paginated data
behavior and query UI state handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/server/src/tests/e2e/ui-components.gallery.spec.ts`:
- Around line 331-364: このテストのスクロール位置アサーションが低すぎます。テスト「virtual media grid
preserves offset while fast scrolling starts a page
fetch」で、1,500pxへ設定した位置の復元確認を実際の目標値に近い下限へ引き上げ、追加読み込み後の確認も restoredTop
から想定される実際の増加量に近い下限へ強化してください。

In `@packages/ui/src/hooks/use-source-media-page.ts`:
- Around line 272-289: Update the createEffect block that reads
mediaQuery.dataUpdatedAt to use the same explicit void-expression style as the
nearby hasNext reference, without changing the surrounding cache synchronization
logic.

---

Outside diff comments:
In `@packages/ui/src/hooks/use-search-page.ts`:
- Line 1: Update the cache-synchronization createEffect blocks in
use-search-page.ts and use-source-media-page.ts to prefix the unused reactive
accesses searchResultQuery.dataUpdatedAt and mediaQuery.dataUpdatedAt with void,
matching the existing void hasNext style while preserving their reactive
dependency behavior.

---

Nitpick comments:
In `@packages/ui/src/hooks/use-search-page.ts`:
- Line 1: Extract the duplicated cache synchronization, InfiniteData signal
updates, Set-based deduplication, and contentState construction from
use-search-page.ts and use-source-media-page.ts into a shared hook such as
useCachedInfiniteQueryResult. Update the searchResultQuery/searchResultData and
mediaQuery/mediaQueryData flows to use that helper while preserving their
existing query options and resulting content state behavior.
- Around line 184-227: Extract the duplicated cache-synchronization effect and
page-result deduplication currently used by searchResults/contentState into a
shared hook or utility, then reuse it from both use-search-page and
use-source-media-page. Preserve the existing query-key cache updates,
placeholder handling, deduplication by media id, and UI state behavior while
removing the duplicated implementations.

In `@packages/ui/src/hooks/use-source-media-page.ts`:
- Around line 260-313: Remove the duplicated cache-synchronization and
result-deduplication logic from the media page flow around mediaQueryData, the
createEffect, mediaResults, and contentState. Reuse the existing shared
implementation or abstraction established by use-search-page.ts, preserving the
current paginated data behavior and query UI state handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eb6ceff8-965b-4dc7-b191-54d5eafa5792

📥 Commits

Reviewing files that changed from the base of the PR and between 53c7204 and 7d8b8ab.

📒 Files selected for processing (8)
  • apps/server/src/tests/e2e/ui-components.gallery.spec.ts
  • apps/server/src/tests/e2e/ui-gallery/src.tsx
  • packages/ui/src/hooks/use-search-page.ts
  • packages/ui/src/hooks/use-source-media-page.ts
  • packages/ui/src/screens/search-screen.tsx
  • packages/ui/src/screens/source-media-screen.tsx
  • packages/ui/src/screens/v2-search-screen.tsx
  • packages/ui/src/screens/v2-source-media-screen.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/ui/src/screens/v2-search-screen.tsx
  • packages/ui/src/screens/v2-source-media-screen.tsx

Comment thread apps/server/src/tests/e2e/ui-components.gallery.spec.ts
Comment thread packages/ui/src/hooks/use-source-media-page.ts
@hmjn023

hmjn023 commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

原因と検証結果

本番で発生していた主因は、メディア一覧の高速スクロール時に実行される次ページ取得が、複数の長時間SSE接続と競合して遅延・滞留していたことでした。

  • V2 shellと各ルートが同じsourceイベントを重複購読していた
  • desktop/mobileのPendingDownloadsIndicatorがimportイベントを重複購読していた
  • detail画面を離れた後のsourceイベント接続が30秒残存していた
  • その状態でmedia/searchのoffset: 200取得とthumbnailリクエストが重なり、page fetch中のSolid Query更新がSuspense境界を再評価していた
  • 結果としてgrid/mainが再生成され、戻り直後はいったん復元されても、高速スクロール時に白画面化してscrollTop=0へ戻っていた

加えて、UI戻り直後に検索状態保存の1秒debounceが未完了だと、戻り時に初期状態へ復元される競合もありました。

対応内容:

  • source/importイベントストリームを共有し、重複接続を排除
  • detail離脱時のsource接続を即時終了
  • ページング結果をresource-backed dataから分離し、検索状態の復元完了前は次ページ取得を抑止
  • 初期検索状態を同期保存
  • layout後に実際のscroll elementを解決
  • UIの「一覧に戻る」もhistory.back()に統一

再発防止として、600件超の実データを使い、検索/source × ブラウザ戻る/UI戻るの4ケースで、offset: 200のAPIを遅延させた高速スクロールE2Eを追加しました。mainとscroll elementのDOM identity、scrollTop維持、page追加後のscrollHeight増加を検証し、開発環境・production buildともに4/4通過しています。

@hmjn023
hmjn023 merged commit 151d75e into develop Aug 6, 2026
1 check passed
@hmjn023
hmjn023 deleted the fix/search-scroll-and-grid-restore branch August 6, 2026 13:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant