Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/lib/components/ClipOverlay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -272,7 +278,6 @@
{gifEnabled}
seenByOthers={clip.seenByOthers}
hideViewBadge={true}
onwatched={handleWatched}
onfavorited={handleFavorite}
onreaction={handleReaction}
onretry={handleRetry}
Expand Down
12 changes: 11 additions & 1 deletion src/lib/components/MeReelView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -134,7 +145,6 @@
{gifEnabled}
seenByOthers={clip.seenByOthers}
hideViewBadge={true}
{onwatched}
{onfavorited}
{onreaction}
{onretry}
Expand Down
41 changes: 3 additions & 38 deletions src/lib/components/ReelItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@
gifEnabled = false,
seenByOthers = false,
hideViewBadge = false,
deferWatched = false,
deferFirstClip = false,
onwatched,
onfavorited,
onreaction,
onretry,
Expand All @@ -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<void>;
onretry: (id: string) => void;
Expand All @@ -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<typeof setTimeout> | null = null;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
63 changes: 15 additions & 48 deletions src/routes/(app)/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string | null>(null);

// Clip overlay (dedicated single-clip view)
let overlayClipId = $state<string | null>(null);
let overlayOpenComments = $state(false);
Expand Down Expand Up @@ -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');
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -648,7 +631,6 @@
$effect(() => {
const targetClipId = $clipOverlaySignal;
if (!targetClipId) return;
console.log('[Feed] clipOverlaySignal fired:', targetClipId, 'setting overlayClipId');
clipOverlaySignal.set(null);
overlayClipId = targetClipId;
});
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -792,7 +773,7 @@
}
}

$effect(() => {
onMount(() => {
isDesktopFeed = matchMedia('(pointer: fine)').matches;
const shareUrl = extractShareTargetUrl();
if (shareUrl) {
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions src/routes/api/clips/[id]/comments/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading