Skip to content

Commit fded7af

Browse files
fix: improve mobile progress bar scrubbing reliability (#139)
* fix: use window-level listeners for progress bar scrubbing on mobile Once a scrub starts, attach pointermove/pointerup listeners to window instead of the bar element. This way the finger can drift vertically off the bar during a drag and it still tracks the horizontal X position. * feat(nav): add unwatched clip count badge to home tab * chore(deps): bump picomatch to 4.0.4 (CVE-2026-33671) * chore: ignore CVE-2026-33671 picomatch in Node base image --------- Co-authored-by: gca <gadams@harrisassoc.com>
1 parent 04472b8 commit fded7af

5 files changed

Lines changed: 52 additions & 21 deletions

File tree

.trivyignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ CVE-2026-29786
1616

1717
# tar node-tar <7.5.11 vulnerability (npm bundled)
1818
CVE-2026-31802
19+
20+
# picomatch ReDoS via crafted glob patterns (npm bundled)
21+
CVE-2026-33671

package-lock.json

Lines changed: 4 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"lint-staged": "^16.4.0",
7373
"mermaid": "^11.13.0",
7474
"phosphor-svelte": "^3.1.0",
75+
"picomatch": "^4.0.4",
7576
"pino-pretty": "^13.1.3",
7677
"prettier": "^3.8.1",
7778
"prettier-plugin-svelte": "^3.5.1",

src/lib/components/ProgressBar.svelte

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,38 +36,50 @@
3636
return ratio * duration;
3737
}
3838
39+
// Track pointer type so we know whether to clear hover on end
40+
let activePointerType: string | null = null;
41+
3942
function handlePointerDown(e: PointerEvent) {
4043
e.preventDefault();
4144
e.stopPropagation();
4245
scrubbing = true;
43-
barEl?.setPointerCapture(e.pointerId);
46+
activePointerType = e.pointerType;
4447
const t = getTimeFromX(e.clientX);
4548
scrubTime = t;
4649
onscrubstart?.();
4750
onseek(t);
51+
// Listen on window so the finger can drift anywhere and we still track X
52+
window.addEventListener('pointermove', handleWindowPointerMove);
53+
window.addEventListener('pointerup', handleWindowPointerUp);
54+
window.addEventListener('pointercancel', handleWindowPointerUp);
4855
}
4956
50-
function handlePointerMove(e: PointerEvent) {
51-
if (scrubbing) {
52-
const t = getTimeFromX(e.clientX);
53-
scrubTime = t;
54-
// Video is paused during scrub, so seek directly on every move — no throttle needed
55-
onseek(t);
56-
} else if (e.pointerType === 'mouse') {
57-
hoverProgress = getTimeFromX(e.clientX);
58-
}
57+
function handleWindowPointerMove(e: PointerEvent) {
58+
const t = getTimeFromX(e.clientX);
59+
scrubTime = t;
60+
onseek(t);
5961
}
6062
61-
function handlePointerUp(e: PointerEvent) {
63+
function handleWindowPointerUp(_e: PointerEvent) {
6264
if (!scrubbing) return;
65+
window.removeEventListener('pointermove', handleWindowPointerMove);
66+
window.removeEventListener('pointerup', handleWindowPointerUp);
67+
window.removeEventListener('pointercancel', handleWindowPointerUp);
6368
if (scrubTime !== null) onseek(scrubTime);
6469
scrubbing = false;
6570
scrubTime = null;
66-
barEl?.releasePointerCapture(e.pointerId);
67-
if (e.pointerType !== 'mouse') hoverProgress = null;
71+
if (activePointerType !== 'mouse') hoverProgress = null;
72+
activePointerType = null;
6873
onscrubend?.();
6974
}
7075
76+
function handlePointerMove(e: PointerEvent) {
77+
// Only used for mouse hover preview when not scrubbing
78+
if (!scrubbing && e.pointerType === 'mouse') {
79+
hoverProgress = getTimeFromX(e.clientX);
80+
}
81+
}
82+
7183
function handlePointerLeave() {
7284
if (!scrubbing) hoverProgress = null;
7385
}
@@ -81,8 +93,6 @@
8193
bind:this={barEl}
8294
onpointerdown={handlePointerDown}
8395
onpointermove={handlePointerMove}
84-
onpointerup={handlePointerUp}
85-
onpointercancel={handlePointerUp}
8696
onpointerleave={handlePointerLeave}
8797
tabindex="0"
8898
role="slider"

src/routes/(app)/+layout.svelte

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
import { addVideoModalOpen } from '$lib/stores/addVideoModal';
66
import { queueSheetOpen } from '$lib/stores/queueSheet';
77
import { homeTapSignal } from '$lib/stores/homeTap';
8-
import { unreadCount, startPolling, stopPolling } from '$lib/stores/notifications';
8+
import {
9+
unreadCount,
10+
unwatchedCount,
11+
startPolling,
12+
stopPolling
13+
} from '$lib/stores/notifications';
914
import { queueCount } from '$lib/stores/queue';
1015
import { globalMuted } from '$lib/stores/mute';
1116
import { initAudioContext } from '$lib/audio/normalizer';
@@ -179,12 +184,22 @@
179184
<nav class="bottom-tabs" class:overlay-mode={isFeed} bind:this={bottomTabsEl}>
180185
{#if isFeed}
181186
<button class="tab active" onclick={() => homeTapSignal.update((n) => n + 1)}>
182-
<HouseIcon size={24} weight="fill" />
187+
<span class="home-icon-wrap">
188+
<HouseIcon size={24} weight="fill" />
189+
{#if $unwatchedCount > 0}
190+
<span class="tab-badge">{$unwatchedCount > 9 ? '9+' : $unwatchedCount}</span>
191+
{/if}
192+
</span>
183193
<span>Home</span>
184194
</button>
185195
{:else}
186196
<a href="/" class="tab">
187-
<HouseIcon size={24} />
197+
<span class="home-icon-wrap">
198+
<HouseIcon size={24} />
199+
{#if $unwatchedCount > 0}
200+
<span class="tab-badge">{$unwatchedCount > 9 ? '9+' : $unwatchedCount}</span>
201+
{/if}
202+
</span>
188203
<span>Home</span>
189204
</a>
190205
{/if}
@@ -373,6 +388,7 @@
373388
border-color: var(--reel-bg-elevated);
374389
}
375390
391+
.home-icon-wrap,
376392
.activity-icon-wrap {
377393
position: relative;
378394
display: flex;

0 commit comments

Comments
 (0)