From 803e51c7b19061b865805a3bc39f314dd9f846aa Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 1 Mar 2026 16:55:53 -0600
Subject: [PATCH 1/9] refactor: use dynamic bottom nav height via
ResizeObserver
Replace hardcoded env(safe-area-inset-bottom) calculations with a
measured --bottom-nav-height CSS variable, adapting to real device
safe-area insets automatically.
---
src/lib/components/ActionSidebar.svelte | 2 +-
src/lib/components/MusicDisc.svelte | 2 +-
src/lib/components/ProgressBar.svelte | 2 +-
src/lib/components/ReelOverlay.svelte | 2 +-
src/lib/components/SkeletonReel.svelte | 4 +--
src/lib/components/ToastStack.svelte | 2 +-
.../components/settings/ClipsManager.svelte | 2 +-
src/routes/(app)/+layout.svelte | 28 ++++++++++++++++---
8 files changed, 32 insertions(+), 12 deletions(-)
diff --git a/src/lib/components/ActionSidebar.svelte b/src/lib/components/ActionSidebar.svelte
index 97be29a..0db02fe 100644
--- a/src/lib/components/ActionSidebar.svelte
+++ b/src/lib/components/ActionSidebar.svelte
@@ -167,7 +167,7 @@
.action-sidebar {
position: absolute;
right: var(--space-lg);
- bottom: calc(148px + env(safe-area-inset-bottom));
+ bottom: calc(var(--bottom-nav-height, 64px) + 68px);
display: flex;
flex-direction: column;
align-items: center;
diff --git a/src/lib/components/MusicDisc.svelte b/src/lib/components/MusicDisc.svelte
index 1dc5591..75cccac 100644
--- a/src/lib/components/MusicDisc.svelte
+++ b/src/lib/components/MusicDisc.svelte
@@ -84,7 +84,7 @@
.music-disc-area {
position: absolute;
right: var(--space-lg);
- bottom: calc(90px + env(safe-area-inset-bottom));
+ bottom: calc(var(--bottom-nav-height, 64px) + 10px);
display: flex;
align-items: center;
gap: var(--space-sm);
diff --git a/src/lib/components/ProgressBar.svelte b/src/lib/components/ProgressBar.svelte
index 54298e9..4bec6b4 100644
--- a/src/lib/components/ProgressBar.svelte
+++ b/src/lib/components/ProgressBar.svelte
@@ -108,7 +108,7 @@
diff --git a/src/routes/api/push/test/+server.ts b/src/routes/api/push/test/+server.ts
new file mode 100644
index 0000000..b30b676
--- /dev/null
+++ b/src/routes/api/push/test/+server.ts
@@ -0,0 +1,30 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { withAuth } from '$lib/server/api-utils';
+import { sendNotification } from '$lib/server/push';
+import { db } from '$lib/server/db';
+import { pushSubscriptions } from '$lib/server/db/schema';
+import { eq } from 'drizzle-orm';
+
+export const POST: RequestHandler = withAuth(async (_event, { user }) => {
+ const subs = await db.query.pushSubscriptions.findMany({
+ where: eq(pushSubscriptions.userId, user.id)
+ });
+
+ if (subs.length === 0) {
+ return json({ error: 'No push subscriptions found' }, { status: 400 });
+ }
+
+ // Wait 10 seconds server-side so the notification arrives even if the user
+ // locks their phone or navigates away
+ await new Promise((resolve) => setTimeout(resolve, 10_000));
+
+ await sendNotification(user.id, {
+ title: 'Test notification',
+ body: 'Push notifications are working!',
+ tag: 'test-notification',
+ url: '/settings'
+ });
+
+ return json({ sent: true, sentAt: Date.now() });
+});
From e9a049b10dc4a544e40ba782bc1abb6b6dbf942e Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 1 Mar 2026 16:59:13 -0600
Subject: [PATCH 4/9] feat: add shortcut validation before save
Validate iCloud shortcut URLs before saving by hitting a validation
endpoint first. Show blockers, soft warnings, and unreachable states
with appropriate save/retry actions. Extract ValidationResults into a
shared component to keep ShortcutManager under the line limit.
---
.../settings/ShortcutManager.svelte | 144 +++++++++-------
.../settings/ValidationResults.svelte | 154 ++++++++++++++++++
src/routes/share/setup/+page.svelte | 49 +++++-
3 files changed, 285 insertions(+), 62 deletions(-)
create mode 100644 src/lib/components/settings/ValidationResults.svelte
diff --git a/src/lib/components/settings/ShortcutManager.svelte b/src/lib/components/settings/ShortcutManager.svelte
index 891107e..e0ad521 100644
--- a/src/lib/components/settings/ShortcutManager.svelte
+++ b/src/lib/components/settings/ShortcutManager.svelte
@@ -9,6 +9,7 @@
import AppleLogoIcon from 'phosphor-svelte/lib/AppleLogoIcon';
import AndroidLogoIcon from 'phosphor-svelte/lib/AndroidLogoIcon';
import CheckCircleIcon from 'phosphor-svelte/lib/CheckCircleIcon';
+ import ValidationResults from './ValidationResults.svelte';
const ICLOUD_SHORTCUT_RE = /^https:\/\/www\.icloud\.com\/shortcuts\/[a-f0-9]{32}\/?$/;
@@ -20,48 +21,73 @@
shortcutToken: string | null;
} = $props();
+ const SOFT_CODES = ['bad_name', 'localhost_url'];
+ const UNREACHABLE_CODES = ['fetch_failed'];
+
let savedUrl = $state(propUrl ?? '');
let shortcutUrl = $state(propUrl ?? '');
let saving = $state(false);
+ let validating = $state(false);
let validationError = $state('');
+ let validationWarnings = $state<{ code: string; message: string }[]>([]);
+ let validated = $state(false);
let token = $state(propToken);
let rotating = $state(false);
let showSheet = $state(false);
const isConfigured = $derived(savedUrl.length > 0);
const isDirty = $derived(shortcutUrl.trim() !== savedUrl);
- const canSave = $derived(isDirty && !saving);
+ const canSave = $derived(isDirty && !saving && !validating);
+
+ const blockers = $derived(
+ validationWarnings.filter(
+ (w) => !SOFT_CODES.includes(w.code) && !UNREACHABLE_CODES.includes(w.code)
+ )
+ );
+ const softWarnings = $derived(validationWarnings.filter((w) => SOFT_CODES.includes(w.code)));
+ const isUnreachable = $derived(
+ validationWarnings.some((w) => UNREACHABLE_CODES.includes(w.code))
+ );
+ const hasBlockers = $derived(blockers.length > 0);
+ const hasSoftOnly = $derived(softWarnings.length > 0 && !hasBlockers && !isUnreachable);
+
+ async function doSave(trimmed: string | null) {
+ saving = true;
+ try {
+ const res = await fetch('/api/group/shortcut', {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ shortcutUrl: trimmed })
+ });
+ if (res.ok) {
+ const data = await res.json();
+ shortcutUrl = data.shortcutUrl ?? '';
+ savedUrl = data.shortcutUrl ?? '';
+ validationWarnings = [];
+ validated = false;
+ toast.success(trimmed ? 'Shortcut link saved' : 'Shortcut link removed');
+ } else {
+ const data = await res.json();
+ toast.error(data.error || 'Failed to save');
+ }
+ } catch {
+ toast.error('Failed to save');
+ } finally {
+ saving = false;
+ }
+ }
- async function saveShortcutUrl() {
+ async function validateAndSave() {
const trimmed = shortcutUrl.trim();
if (trimmed === savedUrl) {
validationError = '';
return;
}
- // Allow clearing the field
+ // Allow clearing the field without validation
if (!trimmed) {
validationError = '';
- saving = true;
- try {
- const res = await fetch('/api/group/shortcut', {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ shortcutUrl: null })
- });
- if (res.ok) {
- shortcutUrl = '';
- savedUrl = '';
- toast.success('Shortcut link removed');
- } else {
- const data = await res.json();
- toast.error(data.error || 'Failed to save');
- }
- } catch {
- toast.error('Failed to save');
- } finally {
- saving = false;
- }
+ await doSave(null);
return;
}
@@ -73,38 +99,49 @@
}
validationError = '';
- saving = true;
+ validating = true;
+ validated = false;
+ validationWarnings = [];
try {
- const res = await fetch('/api/group/shortcut', {
- method: 'PATCH',
+ const res = await fetch('/api/group/shortcut/validate', {
+ method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ shortcutUrl: trimmed })
});
if (res.ok) {
const data = await res.json();
- shortcutUrl = data.shortcutUrl ?? '';
- savedUrl = data.shortcutUrl ?? '';
- toast.success('Shortcut link saved');
+ validationWarnings = data.warnings ?? [];
+ validated = true;
+
+ // Auto-save only when fully clean
+ if (validationWarnings.length === 0) {
+ await doSave(trimmed);
+ }
} else {
const data = await res.json();
- toast.error(data.error || 'Failed to save');
+ toast.error(data.error || 'Validation failed');
}
} catch {
- toast.error('Failed to save');
+ toast.error('Could not validate shortcut');
} finally {
- saving = false;
+ validating = false;
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') {
e.preventDefault();
- if (canSave) saveShortcutUrl();
+ if (canSave) validateAndSave();
}
}
function handleInput() {
if (validationError) validationError = '';
+ // Reset validation state when the user edits
+ if (validated) {
+ validated = false;
+ validationWarnings = [];
+ }
}
async function rotateToken() {
@@ -199,15 +236,17 @@
onkeydown={handleKeydown}
oninput={handleInput}
placeholder="https://www.icloud.com/shortcuts/..."
- disabled={saving}
+ disabled={saving || validating}
/>
- {:else}
-
+ {:else if hasSoftOnly}
+ {#each softWarnings as warning (warning.code + warning.message)}
+
+
+
+ {@html warning.message}
+
+ {/each}
+ doSave(icloudLink.trim())}
+ disabled={savingLink}
+ >
+ {savingLink ? 'Saving…' : 'Save Anyway'}
+
{/if}
{/if}
@@ -786,6 +798,29 @@
color: var(--text-primary);
font-weight: 600;
}
+ .validation-soft {
+ display: flex;
+ align-items: flex-start;
+ gap: var(--space-sm);
+ padding: var(--space-sm) var(--space-md);
+ background: color-mix(in srgb, var(--warning) 8%, transparent);
+ border: 1px solid color-mix(in srgb, var(--warning) 20%, transparent);
+ border-radius: var(--radius-sm);
+ }
+ .validation-soft :global(svg) {
+ flex-shrink: 0;
+ color: var(--warning);
+ margin-top: 2px;
+ }
+ .validation-soft span {
+ font-size: 0.9375rem;
+ color: var(--text-secondary);
+ line-height: 1.5;
+ }
+ .validation-soft span :global(b) {
+ color: var(--text-primary);
+ font-weight: 600;
+ }
.blocker-link {
display: inline;
background: none;
From 00b04bd40cd70a97d83df802b8a5cc70575d2a0a Mon Sep 17 00:00:00 2001
From: Grayson Adams <51373669+GraysonCAdams@users.noreply.github.com>
Date: Sun, 1 Mar 2026 16:59:24 -0600
Subject: [PATCH 5/9] fix: use accent-colored inline SVG for brand logo
Switch brand icon from img element to inline SVG with currentColor so it
inherits the group accent color dynamically.
---
src/lib/assets/icon.svg | 2 +-
src/routes/join/+page.svelte | 11 +++++++++--
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/src/lib/assets/icon.svg b/src/lib/assets/icon.svg
index fbc58c6..4936657 100644
--- a/src/lib/assets/icon.svg
+++ b/src/lib/assets/icon.svg
@@ -1,5 +1,5 @@