From 5ddd5997a286ba274d5b95199e398fbba82aae84 Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Wed, 11 Mar 2026 23:28:30 -0500
Subject: [PATCH 1/3] feat: replace circular reaction wheel with horizontal bar
Redesign the reaction picker as a Facebook-style horizontal pill bar
that expands leftward from the heart button on long-press. Heart is
the rightmost emoji, with drag-to-select and tap-to-pick modes.
Block feed swipe gestures while picker is open using sheetOpen store.
---
src/lib/components/ActionSidebar.svelte | 5 +-
src/lib/components/ReactionPicker.svelte | 87 ++++++++++++++----------
src/routes/(app)/+page.svelte | 4 +-
3 files changed, 57 insertions(+), 39 deletions(-)
diff --git a/src/lib/components/ActionSidebar.svelte b/src/lib/components/ActionSidebar.svelte
index 03b7814..6405627 100644
--- a/src/lib/components/ActionSidebar.svelte
+++ b/src/lib/components/ActionSidebar.svelte
@@ -84,8 +84,9 @@
holdTimer = setTimeout(() => {
holdFired = true;
if (onreactionhold && saveBtnEl) {
- const rect = saveBtnEl.getBoundingClientRect();
- onreactionhold(rect.left + rect.width / 2, rect.top);
+ const circle = saveBtnEl.querySelector('.icon-circle');
+ const rect = (circle ?? saveBtnEl).getBoundingClientRect();
+ onreactionhold(rect.left + rect.width / 2, rect.top + rect.height / 2);
}
}, 350);
}
diff --git a/src/lib/components/ReactionPicker.svelte b/src/lib/components/ReactionPicker.svelte
index 2ab9e6a..490b6ee 100644
--- a/src/lib/components/ReactionPicker.svelte
+++ b/src/lib/components/ReactionPicker.svelte
@@ -1,9 +1,10 @@
+
+
e.stopPropagation()}
+ ontouchmove={(e) => e.stopPropagation()}
+ ontouchend={(e) => e.stopPropagation()}
+ onpointerdown={(e) => e.stopPropagation()}
>
- {#each REACTIONS as emoji, i (emoji)}
+ {#each BAR_EMOJIS as emoji, i (emoji)}
+ {@const delayIndex = BAR_EMOJIS.length - 1 - i}
From 38b37a8696cb9f0cb59d7fe17c144f48c6a86a20 Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Wed, 11 Mar 2026 23:28:46 -0500
Subject: [PATCH 3/3] fix: only mark clips watched on swipe, not on finish
Add PATCH handler for watch percent updates that doesn't create watched
records. Change client sendWatchPercent to use PATCH so progress is
tracked without marking clips as watched prematurely.
---
src/lib/reelPlayback.ts | 20 +++++---------
src/routes/api/clips/[id]/watched/+server.ts | 28 ++++++++++++++++++++
2 files changed, 35 insertions(+), 13 deletions(-)
diff --git a/src/lib/reelPlayback.ts b/src/lib/reelPlayback.ts
index a591ca5..8418047 100644
--- a/src/lib/reelPlayback.ts
+++ b/src/lib/reelPlayback.ts
@@ -40,19 +40,13 @@ export function sendWatchPercent(clipId: string, maxPercent: number): void {
if (maxPercent <= 0) return;
const pct = Math.round(maxPercent);
const body = JSON.stringify({ watchPercent: pct });
- if (navigator.sendBeacon) {
- navigator.sendBeacon(
- `/api/clips/${clipId}/watched`,
- new Blob([body], { type: 'application/json' })
- );
- } else {
- fetch(`/api/clips/${clipId}/watched`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body,
- keepalive: true
- }).catch((err) => console.warn('[watch-beacon]', err));
- }
+ // Use PATCH to update percent without marking as watched (only swipe-past marks watched)
+ fetch(`/api/clips/${clipId}/watched`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body,
+ keepalive: true
+ }).catch((err) => console.warn('[watch-percent]', err));
}
export function startPeriodicWatchUpdate(
diff --git a/src/routes/api/clips/[id]/watched/+server.ts b/src/routes/api/clips/[id]/watched/+server.ts
index 92d4151..0101e2c 100644
--- a/src/routes/api/clips/[id]/watched/+server.ts
+++ b/src/routes/api/clips/[id]/watched/+server.ts
@@ -39,6 +39,34 @@ export const POST: RequestHandler = withClipAuth(async ({ params, request }, { u
return json({ watched: true });
});
+// PATCH: update watchPercent only — creates the row if needed but does NOT count as "watched"
+// Used for periodic progress tracking while user is still viewing
+export const PATCH: RequestHandler = withClipAuth(async ({ params, request }, { user }) => {
+ let watchPercent: number | null = null;
+ try {
+ const body = await request.json();
+ if (typeof body.watchPercent === 'number') {
+ watchPercent = Math.max(0, Math.min(100, Math.round(body.watchPercent)));
+ }
+ } catch {
+ // No body or invalid JSON
+ }
+
+ if (watchPercent === null) {
+ return json({ updated: false });
+ }
+
+ // Only update if the row already exists — don't create a watched record
+ await db
+ .update(watched)
+ .set({
+ watchPercent: sql`MAX(COALESCE(${watched.watchPercent}, 0), ${watchPercent})`
+ })
+ .where(and(eq(watched.clipId, params.id), eq(watched.userId, user.id)));
+
+ return json({ updated: true });
+});
+
export const DELETE: RequestHandler = withClipAuth(async ({ params }, { user }) => {
await db.delete(watched).where(and(eq(watched.clipId, params.id), eq(watched.userId, user.id)));