From 7271cd115467d0eebd6f834e0c234411886860d1 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:17:24 -0500 Subject: [PATCH 1/2] refactor: simplify watched marking to swipe-past model Replace the complex deferred/threshold watched logic with a simpler approach: mark a clip watched when the user swipes past it. ClipOverlay marks immediately on open. Removes deferWatched, deferFirstClip props and associated timers from ReelItem. Cleans up console.logs and uses onMount for feed initialization. --- src/lib/components/ClipOverlay.svelte | 7 ++- src/lib/components/MeReelView.svelte | 12 ++++- src/lib/components/ReelItem.svelte | 41 ++--------------- src/routes/(app)/+page.svelte | 63 +++++++-------------------- 4 files changed, 35 insertions(+), 88 deletions(-) diff --git a/src/lib/components/ClipOverlay.svelte b/src/lib/components/ClipOverlay.svelte index d3bdb02..ee8210d 100644 --- a/src/lib/components/ClipOverlay.svelte +++ b/src/lib/components/ClipOverlay.svelte @@ -197,6 +197,12 @@ }; }); + // Mark the clip as watched immediately when opened in the overlay + $effect(() => { + if (!clip || clip.watched) return; + handleWatched(clip.id); + }); + // Interaction handlers (local state updates) async function handleWatched(id: string) { await markClipWatched(id); @@ -272,7 +278,6 @@ {gifEnabled} seenByOthers={clip.seenByOthers} hideViewBadge={true} - onwatched={handleWatched} onfavorited={handleFavorite} onreaction={handleReaction} onretry={handleRetry} diff --git a/src/lib/components/MeReelView.svelte b/src/lib/components/MeReelView.svelte index b9323f3..39d6d26 100644 --- a/src/lib/components/MeReelView.svelte +++ b/src/lib/components/MeReelView.svelte @@ -97,6 +97,17 @@ return () => observer.disconnect(); }); + // Mark a clip as watched when the user swipes past it (forward only) + let prevActiveIndex = 0; + $effect(() => { + const curr = activeIndex; + if (curr > prevActiveIndex) { + const clip = clips[prevActiveIndex]; + if (clip && !clip.watched) onwatched(clip.id); + } + prevActiveIndex = curr; + }); + function close() { if (dismissed) return; dismissed = true; @@ -134,7 +145,6 @@ {gifEnabled} seenByOthers={clip.seenByOthers} hideViewBadge={true} - {onwatched} {onfavorited} {onreaction} {onretry} diff --git a/src/lib/components/ReelItem.svelte b/src/lib/components/ReelItem.svelte index 5d4b9b5..00597b7 100644 --- a/src/lib/components/ReelItem.svelte +++ b/src/lib/components/ReelItem.svelte @@ -57,9 +57,6 @@ gifEnabled = false, seenByOthers = false, hideViewBadge = false, - deferWatched = false, - deferFirstClip = false, - onwatched, onfavorited, onreaction, onretry, @@ -75,9 +72,6 @@ gifEnabled?: boolean; seenByOthers?: boolean; hideViewBadge?: boolean; - deferWatched?: boolean; - deferFirstClip?: boolean; - onwatched: (id: string) => void; onfavorited: (id: string) => void; onreaction: (clipId: string, emoji: string) => Promise; onretry: (id: string) => void; @@ -86,7 +80,6 @@ } = $props(); let itemEl: HTMLDivElement | null = $state(null); - let hasMarkedWatched = $state(false); let muted = $derived($globalMuted); let showMuteIndicator = $state(false); let muteIndicatorTimer: ReturnType | null = null; @@ -204,9 +197,7 @@ if (postEngagementTimer) clearTimeout(postEngagementTimer); if (pillTimer) clearTimeout(pillTimer); scrubSeekedCleanup?.(); - if ((!deferWatched && !deferFirstClip) || hasMarkedWatched) { - sendWatchPercent(clip.id, maxPercent); - } + sendWatchPercent(clip.id, maxPercent); }); // Contributor pill: expand when a different contributor's clip becomes active @@ -248,30 +239,6 @@ $effect(() => { if (active) feedUiHidden.set(uiHidden); }); - $effect(() => { - if (!active || clip.watched || hasMarkedWatched || deferWatched || deferFirstClip) return; - const timer = setTimeout(() => { - hasMarkedWatched = true; - onwatched(clip.id); - }, 3000); - return () => clearTimeout(timer); - }); - // Deferred watch: mark watched when 50% or 10s threshold is met - $effect(() => { - if (!deferWatched || !active || clip.watched || hasMarkedWatched) return; - if ((duration > 0 && currentTime / duration >= 0.5) || currentTime >= 10) { - hasMarkedWatched = true; - onwatched(clip.id); - } - }); - // First clip deferral: only mark watched at 40% progress - $effect(() => { - if (!deferFirstClip || !active || clip.watched || hasMarkedWatched) return; - if (duration > 0 && currentTime / duration >= 0.4) { - hasMarkedWatched = true; - onwatched(clip.id); - } - }); let hasMarkedReactionsRead = $state(false); $effect(() => { if (!active || hasMarkedReactionsRead) return; @@ -293,15 +260,13 @@ wasActive = true; } else if (wasActive) { wasActive = false; - if (!deferWatched || hasMarkedWatched) { - sendWatchPercent(clip.id, maxPercent); - } + sendWatchPercent(clip.id, maxPercent); maxPercent = 0; } }); // Send watch percent to server periodically while active $effect(() => { - if (!active || (deferWatched && !hasMarkedWatched)) return; + if (!active) return; return startPeriodicWatchUpdate(clip.id, () => maxPercent); }); // Fetch comment previews for cycling prompt bar diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte index 8268d74..edc4bab 100644 --- a/src/routes/(app)/+page.svelte +++ b/src/routes/(app)/+page.svelte @@ -25,6 +25,7 @@ import { feedUiHidden } from '$lib/stores/uiHidden'; import { anySheetOpen } from '$lib/stores/sheetOpen'; import { get } from 'svelte/store'; + import { onMount } from 'svelte'; import { page } from '$app/state'; import { basename } from '$lib/utils'; import type { FeedClip } from '$lib/types'; @@ -57,17 +58,6 @@ let isDesktopFeed = $state(false); const renderWindow = $derived(isDesktopFeed ? 3 : 2); - // Defer watched marking for the last unwatched clip - const isLastUnwatched = $derived( - filter === 'unwatched' && - !hasMore && - clips.length > 0 && - clips.filter((c) => !c.watched).length === 1 - ); - - // Defer watched marking for the first loaded clip until 40% watched or swiped past - let firstClipId = $state(null); - // Clip overlay (dedicated single-clip view) let overlayClipId = $state(null); let overlayOpenComments = $state(false); @@ -139,7 +129,6 @@ clips = data.clips; hasMore = data.hasMore; currentOffset = data.clips.length; - firstClipId = data.clips.length > 0 ? data.clips[0].id : null; } else { toast.error('Failed to load clips'); } @@ -161,14 +150,14 @@ } async function markWatched(clipId: string) { - const wasUnwatched = clips.find((c) => c.id === clipId && !c.watched); - await markClipWatched(clipId); + const clip = clips.find((c) => c.id === clipId); + if (!clip || clip.watched) return; + // Optimistic update before the API call — prevents double-calls during async gap clips = clips.map((c) => - c.id === clipId - ? { ...c, watched: true, viewCount: c.watched ? c.viewCount : c.viewCount + 1 } - : c + c.id === clipId ? { ...c, watched: true, viewCount: c.viewCount + 1 } : c ); - if (wasUnwatched) fetchUnwatchedCount(); + fetchUnwatchedCount(); + await markClipWatched(clipId); } async function toggleFavorite(clipId: string) { @@ -583,21 +572,15 @@ } }); - // Mark deferred last clip as watched when user swipes to end slide + // Mark a clip as watched when the user swipes past it (forward only) + let prevActiveIndex = 0; $effect(() => { - if (filter === 'unwatched' && !hasMore && activeIndex === clips.length && clips.length > 0) { - const lastClip = clips[clips.length - 1]; - if (!lastClip.watched) markWatched(lastClip.id); - } - }); - - // Mark first clip as watched when user swipes past it - $effect(() => { - if (firstClipId && activeIndex > 0) { - const firstClip = clips.find((c) => c.id === firstClipId); - if (firstClip && !firstClip.watched) markWatched(firstClipId); - firstClipId = null; + const curr = activeIndex; + if (curr > prevActiveIndex) { + const clip = clips[prevActiveIndex]; + if (clip && !clip.watched) markWatched(clip.id); } + prevActiveIndex = curr; }); $effect(() => { @@ -648,7 +631,6 @@ $effect(() => { const targetClipId = $clipOverlaySignal; if (!targetClipId) return; - console.log('[Feed] clipOverlaySignal fired:', targetClipId, 'setting overlayClipId'); clipOverlaySignal.set(null); overlayClipId = targetClipId; }); @@ -718,7 +700,6 @@ } function handleOverlayDismiss() { - console.log('[Feed] handleOverlayDismiss, was:', overlayClipId); overlayClipId = null; overlayOpenComments = false; // Refresh feed to reflect any changes made in the overlay @@ -792,7 +773,7 @@ } } - $effect(() => { + onMount(() => { isDesktopFeed = matchMedia('(pointer: fine)').matches; const shareUrl = extractShareTargetUrl(); if (shareUrl) { @@ -820,18 +801,7 @@ const params = new URLSearchParams(window.location.search); const deepClipId = params.get('clip'); const deepComments = params.get('comments') === 'true'; - console.log('[Feed] deep-link check:', { - deepClipId, - deepComments, - search: window.location.search - }); if (deepClipId) { - console.log( - '[Feed] deep-link: opening overlay for clip', - deepClipId, - 'comments:', - deepComments - ); // Clean URL without triggering navigation const clean = new URL(window.location.href); clean.searchParams.delete('clip'); @@ -957,9 +927,6 @@ {autoScroll} {gifEnabled} seenByOthers={clip.seenByOthers} - deferWatched={isLastUnwatched && !clip.watched} - deferFirstClip={clip.id === firstClipId && !clip.watched} - onwatched={markWatched} onfavorited={toggleFavorite} onreaction={handleReaction} onretry={retryDownload} From 6905278a8bb5161465414e330d275f0932e27969 Mon Sep 17 00:00:00 2001 From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com> Date: Wed, 11 Mar 2026 22:17:32 -0500 Subject: [PATCH 2/2] fix: sort comments oldest-first when hearts are equal --- src/routes/api/clips/[id]/comments/+server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/api/clips/[id]/comments/+server.ts b/src/routes/api/clips/[id]/comments/+server.ts index 25a007e..0a040ba 100644 --- a/src/routes/api/clips/[id]/comments/+server.ts +++ b/src/routes/api/clips/[id]/comments/+server.ts @@ -98,11 +98,11 @@ export const GET: RequestHandler = withClipAuth(async ({ params }, { user }) => }; } - // Sort top-level: hearts desc, then newest first + // Sort top-level: hearts desc, then oldest first topLevel.sort((a, b) => { const heartDiff = (heartCounts.get(b.id) || 0) - (heartCounts.get(a.id) || 0); if (heartDiff !== 0) return heartDiff; - return b.createdAt.getTime() - a.createdAt.getTime(); + return a.createdAt.getTime() - b.createdAt.getTime(); }); // Build response with nested replies (sorted chronologically)