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
10 changes: 6 additions & 4 deletions src/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
<meta property="og:image" content="/icons/icon-512.png" />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icons/apple-touch-icon.png" />
<link rel="icon" id="dynamic-favicon" type="image/svg+xml" href="/icon/favicon.svg" />
<link rel="icon" href="/favicon-32.png" sizes="32x32" type="image/png" />
<link rel="icon" href="/favicon-16.png" sizes="16x16" type="image/png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Sora:wght@400;600;700;800&family=Unbounded:wght@800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Sora:wght@400;600;700;800&display=swap" rel="stylesheet" />
%sveltekit.head%
<script>
(function() {
Expand All @@ -32,8 +32,10 @@
if (a) {
try {
var c = JSON.parse(decodeURIComponent(a[1]));
document.documentElement.style.setProperty('--accent-primary', c.hex);
document.documentElement.style.setProperty('--accent-primary-dark', c.dark);
if (/^#[0-9a-fA-F]{3,8}$/.test(c.hex) && /^#[0-9a-fA-F]{3,8}$/.test(c.dark)) {
document.documentElement.style.setProperty('--accent-primary', c.hex);
document.documentElement.style.setProperty('--accent-primary-dark', c.dark);
}
} catch(e) { /* ignore malformed cookie */ }
}
})();
Expand Down
67 changes: 67 additions & 0 deletions src/lib/__tests__/phone.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { describe, it, expect } from 'vitest';
import { rawDigits, formatPhone, toE164 } from '../phone';

describe('rawDigits', () => {
it('strips non-digit characters', () => {
expect(rawDigits('(555) 123-4567')).toBe('5551234567');
});

it('returns empty string for empty input', () => {
expect(rawDigits('')).toBe('');
});

it('returns digits only from mixed input', () => {
expect(rawDigits('+1 (555) 123-4567')).toBe('15551234567');
});

it('passes through pure digit strings unchanged', () => {
expect(rawDigits('1234567890')).toBe('1234567890');
});
});

describe('formatPhone', () => {
it('returns empty string for no digits', () => {
expect(formatPhone('')).toBe('');
});

it('formats 1-3 digits with opening paren', () => {
expect(formatPhone('5')).toBe('(5');
expect(formatPhone('55')).toBe('(55');
expect(formatPhone('555')).toBe('(555');
});

it('formats 4-6 digits with area code and space', () => {
expect(formatPhone('5551')).toBe('(555) 1');
expect(formatPhone('55512')).toBe('(555) 12');
expect(formatPhone('555123')).toBe('(555) 123');
});

it('formats 7-10 digits with full formatting', () => {
expect(formatPhone('5551234')).toBe('(555) 123-4');
expect(formatPhone('55512345')).toBe('(555) 123-45');
expect(formatPhone('555123456')).toBe('(555) 123-456');
expect(formatPhone('5551234567')).toBe('(555) 123-4567');
});

it('truncates digits beyond 10', () => {
expect(formatPhone('55512345678')).toBe('(555) 123-4567');
});
});

describe('toE164', () => {
it('converts formatted phone to E.164', () => {
expect(toE164('(555) 123-4567')).toBe('+15551234567');
});

it('converts raw digits to E.164', () => {
expect(toE164('5551234567')).toBe('+15551234567');
});

it('handles empty string', () => {
expect(toE164('')).toBe('+1');
});

it('handles partial input', () => {
expect(toE164('(555')).toBe('+1555');
});
});
47 changes: 29 additions & 18 deletions src/lib/components/ActivitySheet.svelte
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
<script lang="ts">
import { goto, pushState } from '$app/navigation';
import { pushState } from '$app/navigation';
import { resolve } from '$app/paths';
import { relativeTime } from '$lib/utils';
import { fetchUnreadCount } from '$lib/stores/notifications';
import { viewClipSignal, openCommentsSignal } from '$lib/stores/toasts';
import XIcon from 'phosphor-svelte/lib/XIcon';
import BellIcon from 'phosphor-svelte/lib/BellIcon';

const { ondismiss }: { ondismiss: () => void } = $props();

interface Notification {
id: string;
type: 'reaction' | 'comment' | 'mention';
type: 'reaction' | 'comment' | 'reply' | 'mention';
clipId: string;
emoji: string | null;
commentPreview: string | null;
Expand Down Expand Up @@ -79,6 +80,7 @@
return () => {
document.body.style.overflow = '';
window.removeEventListener('popstate', handlePopState);
if (fadeTimer) clearTimeout(fadeTimer);
if (!closedViaBack) history.back();
};
});
Expand All @@ -88,6 +90,8 @@
loadNotifications();
});

let fadeTimer: ReturnType<typeof setTimeout> | null = null;

async function loadNotifications() {
const res = await fetch('/api/notifications?limit=50');
if (res.ok) {
Expand All @@ -96,32 +100,36 @@
}
loading = false;

// Mark all as read on server
await fetch('/api/notifications/mark-read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ all: true })
});
fetchUnreadCount();

// Fade out unread backgrounds after a short delay
setTimeout(() => {
items = items.map((n) => ({ ...n, read: true }));
}, 1000);
// Only mark as read if fetch succeeded
if (res.ok) {
await fetch('/api/notifications/mark-read', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ all: true })
});
fetchUnreadCount();

// Fade out unread backgrounds after a short delay
fadeTimer = setTimeout(() => {
items = items.map((n) => ({ ...n, read: true }));
}, 1000);
}
}

function dismiss() {
visible = false;
setTimeout(() => ondismiss(), 300);
}

function handleNotificationClick(e: Event, clipId: string) {
function handleNotificationClick(e: Event, n: Notification) {
e.preventDefault();
visible = false;
setTimeout(() => {
ondismiss();
// eslint-disable-next-line svelte/no-navigation-without-resolve -- query params appended to resolved base
goto(`${resolve('/')}?clip=${clipId}`);
viewClipSignal.set(n.clipId);
if (n.type !== 'reaction') {
openCommentsSignal.set(n.clipId);
}
}, 300);
}

Expand All @@ -132,6 +140,9 @@
if (n.type === 'mention') {
return 'mentioned you';
}
if (n.type === 'reply') {
return 'replied to your comment';
}
return 'commented on your clip';
}
</script>
Expand Down Expand Up @@ -170,7 +181,7 @@
class="notification-item"
class:unread={!n.read}
style="animation-delay: {Math.min(i, 15) * 30}ms"
onclick={(e) => handleNotificationClick(e, n.clipId)}
onclick={(e) => handleNotificationClick(e, n)}
>
<div class="actor-avatar">
{#if n.actorAvatar}
Expand Down
Loading
Loading