fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix: pictique lazy scroll - #648

Merged
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling
Jan 5, 2026
Merged

fix: pictique lazy scroll#648
coodos merged 4 commits into
mainfrom
fix/pictique-lazy-scrolling

Conversation

@sosweetham

@sosweethamsosweetham commented Jan 4, 2026

Copy link
Copy Markdown
Member

Description of change

Implements lazy load on scroll in pictique

Issue Number

closes#647

Type of change

  • Fix (a change which fixes an issue)

How the change has been tested

Manual

Change checklist

  • I have ensured that the CI Checks pass locally
  • I have removed any unnecessary logic
  • My code is well documented
  • I have signed my commits
  • My code follows the pattern of the application
  • I have self reviewed my code

Summary by CodeRabbit

  • New Features

    • Infinite scrolling that auto-loads additional posts via a sentinel.
    • "Loading more posts..." indicator, "No more posts to load" message, and feed-reset capability.
  • Bug Fixes / Improvements

    • Clearer initial vs. append loading states for smoother UX.
    • More reliable, page-aware pagination and response handling.
    • Optimistic like updates and consistent feed refresh after creating posts.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitaiBot commented Jan 4, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds paginated infinite-scroll to the Pictique feed: new pagination stores (isLoadingMore, currentPage, hasMore), resetFeed() and loadMoreFeed() functions, and fetchFeed(page, limit, append) which supports append mode and separate loading states. The home page now uses an IntersectionObserver sentinel and optimistic like updates.

Changes

Cohort / File(s)Summary
Feed state & API logic
platforms/pictique/src/lib/stores/posts.ts
Added stores: isLoadingMore, currentPage, hasMore. Added resetFeed() and loadMoreFeed(). fetchFeed(page = 1, limit = 10, append = false) supports append mode, distinct loading flags, robust response parsing, and updates currentPage/hasMore. createPost() resets feed before reloading.
Home page UI & infinite scroll
platforms/pictique/src/routes/(protected)/home/+page.svelte
Replaced onScroll pagination with IntersectionObserver sentinel that calls loadMoreFeed() when visible and allowed. Expanded imports to include new stores/functions. onMount resets feed and fetches page 1. Added isLoadingMore UI, “No more posts” message, and optimistic like mutation with revert on error.
Imports / minor adjustments
...
Added get from svelte/store for non-reactive reads; minor formatting and import tweaks to support new logic.

Sequence Diagram

sequenceDiagram
participant User
participant Home as Home Page
participant Store as Posts Store
participant API as Backend API
User->>Home: Open feed
Home->>Store: resetFeed()
Store-->>Store: clear posts, currentPage=0, hasMore=true
Home->>Store: fetchFeed(1,10,false)
activate Store
Store->>API: GET /posts?page=1&limit=10
API-->>Store: { posts, meta }
Store-->>Store: set posts, currentPage=1, hasMore=(meta.hasMore)
deactivate Store
Note over User,Home: User scrolls / viewport reaches sentinel
Home->>Home: IntersectionObserver fires (if hasMore & !isLoading/isLoadingMore)
Home->>Store: loadMoreFeed()
activate Store
Store-->>Store: isLoadingMore=true
Store->>API: GET /posts?page=2&limit=10
API-->>Store: { posts, meta }
Store-->>Store: append posts, currentPage=2, isLoadingMore=false, update hasMore
deactivate Store
Home->>User: Render appended posts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • coodos
  • ananyayaya129

Poem

🐰
I hopped the feed from top to toe,
Ten tiny posts where thousands did flow,
A sentinel blinked — more came in store,
I nudged the page and found one click more,
Hooray for paging, carrots evermore! 🥕

Pre-merge checks

✅ Passed checks (5 passed)
Check nameStatusExplanation
Title check✅ PassedThe title 'fix: pictique lazy scroll' concisely describes the main change of implementing lazy/infinite scrolling for the Pictique feed, which is the primary objective of this pull request.
Description check✅ PassedThe description follows the template with all required sections completed: description of change, issue number, type of change, testing approach, and a completed checklist.
Linked Issues check✅ PassedThe PR successfully implements infinite scroll pagination for Pictique. It fetches 10 posts initially (instead of 1000), provides pagination with loadMoreFeed, and triggers loading on scroll approach as requested in issue #647.
Out of Scope Changes check✅ PassedAll changes directly support the pagination and lazy-scroll implementation objective. New stores manage pagination state, fetchFeed supports pagination with configurable page sizes, and UI uses IntersectionObserver to trigger loads at scroll threshold.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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 and usage tips.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
platforms/pictique/src/lib/stores/posts.ts (1)

77-80: Simplify redundant array mapping.

images.map((img) => img) is an identity mapping that just creates a shallow copy. If a copy is needed, [...images] is clearer; otherwise, just use images directly.

🔎 Suggested simplification
 const response = await apiClient.post("/api/posts", {
text,
- images: images.map((img) => img),+ images,
});
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Consider removing redundant scroll handler.

Both the IntersectionObserver (sentinel) and this onScroll handler trigger loadMoreFeed() under the same conditions. The observer is more efficient and already handles the use case. The scroll handler adds redundancy but also extra event overhead.

If kept as a fallback for browser compatibility, this is fine. Otherwise, consider removing it to simplify the code.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fdea04 and bd43356.

📒 Files selected for processing (2)
  • platforms/pictique/src/lib/stores/posts.ts
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (10)
platforms/pictique/src/lib/stores/posts.ts (5)

1-11: LGTM!

The new stores (isLoadingMore, currentPage, hasMore) are well-structured for managing pagination state alongside the existing stores.


16-20: LGTM!

Clean reset function that properly initializes all pagination-related state.


22-59: LGTM!

The pagination logic is well-implemented:

  • Separate loading states for initial vs. append loads
  • Robust response parsing for different API response structures
  • hasMore calculation correctly handles edge cases (last page, partial results)
  • Error handling prevents infinite retry loops by setting hasMore to false

61-71: LGTM!

The guard conditions properly prevent duplicate requests and the use of get() is appropriate for reading store values in this async context.


92-101: LGTM!

Clean error handling that properly propagates failures to the caller.

platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-34: LGTM!

Imports and state declarations are well-organized for the infinite scroll implementation.


35-60: LGTM!

Well-implemented IntersectionObserver pattern with proper cleanup. Using rootMargin: '200px' for early triggering ensures smooth loading before the user reaches the bottom.


143-156: LGTM!

Proper lifecycle management with passive scroll listener and thorough cleanup of both the listener and the debounce timeout.


158-178: LGTM!

Good UX consideration to auto-load more content if the initial batch doesn't fill the viewport. The setTimeout approach for waiting on DOM measurements is a common pattern.


183-234: LGTM!

Well-structured conditional rendering:

  • Initial loading only shows when no posts exist (prevents flicker on pagination)
  • Clear feedback for "loading more" and "no more posts" states
  • Sentinel element properly gated by hasMore

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @platforms/pictique/src/routes/(protected)/home/+page.svelte:
- Around line 143-156: The $effect block that attaches and cleans up the scroll
listener (referencing listElement, onScroll, scrollTimeout, and $effect) should
be deleted if you remove the redundant onScroll handler; remove this entire
reactive effect to avoid attaching a now-nonexistent listener and cleaning up
unused scrollTimeout state.
- Around line 158-178: The current onMount block uses a 100ms setTimeout to
check listElement.scrollHeight and possibly call loadMoreFeed, which can run
before images load; remove that timeout-check block (the entire setTimeout and
its nested scrollHeight/clientHeight/hasMore/isLoading/isLoadingMore logic) and
rely on the existing IntersectionObserver sentinel to trigger loadMoreFeed when
needed; ensure resetFeed(), fetchFeed(1, 10, false) and fetchProfile() still run
on mount and keep loadMoreFeed, listElement, hasMore, isLoading, and
isLoadingMore intact for the sentinel to consult.
- Around line 200-246: The rollback uses a stale postIndex captured earlier
which can be invalid if the posts array changes; instead capture the post.id
(e.g., const targetPostId = post.id) when performing the optimistic update and
in the catch handler locate the post by id from the posts store (findIndex(p =>
p.id === targetPostId)) before reverting likedBy to originalLikedBy; update
references to postIndex in the like handler and rollback code accordingly and
keep toggleLike, posts.update, originalLikedBy and profile logic the same.
- Around line 35-60: The IntersectionObserver in sentinel currently sets root:
listElement which can be undefined and conflicts with the existing onScroll
logic; remove the root option from the observer options so it uses the viewport,
delete the onScroll function and its associated reactive effect that triggers
loadMoreFeed (keeping only any cleanup for scrollTimeout), and ensure sentinel
only calls loadMoreFeed() when entry.isIntersecting and the existing guards
(get(hasMore), get(isLoading), get(isLoadingMore)) pass.
♻️ Duplicate comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

62-89: Redundant with IntersectionObserver sentinel.

This scroll handler duplicates the loading logic already implemented by the IntersectionObserver sentinel function. Both mechanisms use a 200px threshold and can trigger loadMoreFeed() simultaneously, potentially causing race conditions or duplicate requests.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd43356 and 9b46f8b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (5)
platforms/pictique/src/routes/(protected)/home/+page.svelte (5)

7-24: LGTM!

The new imports for pagination stores and get utility are appropriate for implementing infinite scroll functionality.


27-33: LGTM!

The new state variables for the sentinel element and scroll timeout are correctly typed and initialized.


183-183: LGTM!

The refined loading condition correctly shows the initial loading message only when no posts exist yet, avoiding confusion with the "Loading more posts..." state.


261-269: LGTM!

The conditional rendering for loading states is well-structured with mutually exclusive conditions that prevent conflicting UI states.


289-289: Minor: Class reordering has no functional impact.

The reordering of Tailwind class tokens doesn't affect the rendered output.

Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte Outdated
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte
Comment threadplatforms/pictique/src/routes/(protected)/home/+page.svelte

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
platforms/pictique/src/routes/(protected)/home/+page.svelte (3)

7-24: Unused import: currentPage

currentPage is imported but not referenced anywhere in this component. If it's only used internally by loadMoreFeed, consider removing it from the import list.

🔎 Proposed fix
 import {
- currentPage,
error,
fetchFeed,
hasMore,
isLoading,
isLoadingMore,
loadMoreFeed,
posts,
resetFeed,
toggleLike
} from '$lib/stores/posts';

27-27: Unused binding: sentinelElement

The sentinelElement variable is bound on line 219 but never read. Since the use:sentinel action receives the node directly, this binding is unnecessary.

🔎 Proposed fix

Remove the state declaration:

-let sentinelElement: HTMLElement | undefined = $state();

And remove the binding on line 219:

-<li class="h-1 w-full" bind:this={sentinelElement} use:sentinel></li>+<li class="h-1 w-full" use:sentinel></li>

26-26: Unused binding: listElement

With the scroll handler removed (as suggested in past reviews), listElement is now only bound but never read. Consider removing it.

🔎 Proposed fix

Remove the declaration:

-let listElement: HTMLElement;

And remove the binding on line 122:

-<ul bind:this={listElement} class="hide-scrollbar h-[100vh] overflow-auto">+<ul class="hide-scrollbar h-[100vh] overflow-auto">
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b46f8b and f5dcb7b.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

34-59: LGTM!

The IntersectionObserver implementation is clean:

  • Uses viewport as root (past issue addressed)
  • Proper guard conditions before triggering load
  • Correct cleanup via destroy()

114-118: LGTM!

The onMount logic is clean and addresses the PR objective of loading a small initial batch (~10 posts). The sentinel will handle additional loading if the viewport isn't filled.


123-123: LGTM!

Correctly shows the initial loading state only when there are no posts yet, avoiding flicker during pagination.


140-197: Optimistic update implementation looks good.

The critical issue from the past review (index-based rollback) has been properly addressed:

  • Uses targetPostId for reliable lookup in both update and rollback
  • Correctly captures originalLikedBy before mutation
  • Finds post by ID in both the optimistic update and error rollback paths

212-220: LGTM!

The loading states and sentinel rendering are well-coordinated:

  • Sentinel is hidden during loading, preventing duplicate triggers
  • Clear feedback for "no more posts" state
  • Conditions are mutually exclusive to avoid conflicting UI states

240-240: Minor class reordering - no functional change.

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
platforms/pictique/src/routes/(protected)/home/+page.svelte (1)

137-205: Good optimistic update implementation with proper ID-based rollback.

The like handler correctly captures targetPostId upfront and uses findIndex inside the posts.update callback during rollback, ensuring the correct post is reverted even if the array has changed. This addresses the previous feedback about index-based rollback being unreliable.

Minor edge case: Rapid consecutive clicks could cause originalLikedBy to be captured at different points in time, leading to potential state inconsistencies if an earlier request fails after a later one modifies the same post. Consider adding a per-post loading flag or debouncing the like action if this becomes a UX issue.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dcb7b and 5783c4d.

📒 Files selected for processing (1)
  • platforms/pictique/src/routes/(protected)/home/+page.svelte
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: lint
  • GitHub Check: build
🔇 Additional comments (6)
platforms/pictique/src/routes/(protected)/home/+page.svelte (6)

7-23: LGTM!

The imports correctly bring in the pagination stores (hasMore, isLoading, isLoadingMore) and functions (loadMoreFeed, resetFeed), along with get from svelte/store for accessing store values in non-reactive contexts like the IntersectionObserver callback.


31-56: Well-implemented sentinel action.

The IntersectionObserver correctly uses the viewport as root (addressing the previous feedback), includes appropriate guard conditions to prevent duplicate loads, and properly cleans up via observer.disconnect() in the destroy callback.


111-115: Clean initialization flow.

The onMount correctly resets the feed state and fetches the first page. Not awaiting fetchFeed allows profile loading to proceed in parallel. The sentinel observer will naturally trigger additional loads if the initial content doesn't fill the viewport.


119-120: LGTM!

The loading condition correctly shows the loading message only during initial load when no posts exist yet, preserving existing content visibility during pagination.


220-228: LGTM!

The conditional rendering is well-structured: loading indicator during pagination, end-of-feed message when exhausted, and sentinel element when more content is available. The sentinel's minimal height ensures it doesn't affect layout while still triggering the observer.


248-248: No functional change.

Class reordering doesn't affect behavior or styling.

@coodos
coodos merged commit db6ea53 into mainJan 5, 2026
4 checks passed
@coodos
coodos deleted the fix/pictique-lazy-scrolling branch January 5, 2026 09:12
Sign up for freeto 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.

[feature] Pictique Infinite Scroll

2 participants

@sosweetham@coodos