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} 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)