diff --git a/src/app.d.ts b/src/app.d.ts
index 78130c5..77f3d3d 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -11,6 +11,7 @@ declare global {
}
interface PageState {
sheet?: string;
+ clipOverlay?: string;
}
}
}
diff --git a/src/lib/components/ActionSidebar.svelte b/src/lib/components/ActionSidebar.svelte
index 0db02fe..b0a7232 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(var(--bottom-nav-height, 64px) + 68px);
+ bottom: calc(var(--bottom-nav-height, 64px) + 88px);
display: flex;
flex-direction: column;
align-items: center;
diff --git a/src/lib/components/ActivitySheet.svelte b/src/lib/components/ActivitySheet.svelte
index 3106380..81cd236 100644
--- a/src/lib/components/ActivitySheet.svelte
+++ b/src/lib/components/ActivitySheet.svelte
@@ -3,7 +3,7 @@
import { resolve } from '$app/paths';
import { relativeTime } from '$lib/utils';
import { fetchUnreadCount } from '$lib/stores/notifications';
- import { viewClipSignal, openCommentsSignal } from '$lib/stores/toasts';
+ import { clipOverlaySignal, openCommentsSignal } from '$lib/stores/toasts';
import XIcon from 'phosphor-svelte/lib/XIcon';
import BellIcon from 'phosphor-svelte/lib/BellIcon';
@@ -126,7 +126,7 @@
visible = false;
setTimeout(() => {
ondismiss();
- viewClipSignal.set(n.clipId);
+ clipOverlaySignal.set(n.clipId);
if (n.type !== 'reaction') {
openCommentsSignal.set(n.clipId);
}
diff --git a/src/lib/components/AddVideo.svelte b/src/lib/components/AddVideo.svelte
index 8b169a3..aa1ed33 100644
--- a/src/lib/components/AddVideo.svelte
+++ b/src/lib/components/AddVideo.svelte
@@ -8,22 +8,21 @@
isPlatformAllowed
} from '$lib/url-validation';
import { addToast } from '$lib/stores/toasts';
+ import { showShortcutNudge } from '$lib/stores/shortcutNudge';
import { page } from '$app/stores';
import type { GroupMember } from '$lib/types';
import DownloadSimpleIcon from 'phosphor-svelte/lib/DownloadSimpleIcon';
import ClipboardIcon from 'phosphor-svelte/lib/ClipboardIcon';
import XIcon from 'phosphor-svelte/lib/XIcon';
import ArrowRightIcon from 'phosphor-svelte/lib/ArrowRightIcon';
+ import LightbulbIcon from 'phosphor-svelte/lib/LightbulbIcon';
const {
onsubmitted,
initialUrl,
members = []
}: {
- onsubmitted?: (
- clip: { id: string; status: string; contentType: string },
- caption: string
- ) => void;
+ onsubmitted?: (clip: { id: string; status: string; contentType: string }) => void;
initialUrl?: string;
members?: GroupMember[];
} = $props();
@@ -132,7 +131,7 @@
contentType: data.clip.contentType,
autoDismiss: 0
});
- onsubmitted?.(data.clip, '');
+ onsubmitted?.(data.clip);
} catch {
error = 'Something went wrong';
} finally {
@@ -229,6 +228,14 @@
{platformLabel(url.trim())} links aren't allowed in this group
{/if}
+
+ {#if $showShortcutNudge}
+
+
+
+ Share clips faster from other apps
+
+ {/if}
{/if}
@@ -240,7 +247,6 @@
gap: var(--space-md);
padding: var(--space-sm) var(--space-lg) var(--space-lg);
}
-
.clipboard-suggestion {
display: flex;
align-items: center;
@@ -326,8 +332,6 @@
transform: translateY(0);
}
}
-
- /* iMessage-style compose fields */
.compose-fields {
width: 100%;
background: var(--bg-elevated);
@@ -338,25 +342,21 @@
border-color 0.2s ease,
box-shadow 0.2s ease;
}
-
.compose-fields:focus-within {
border-color: var(--accent-primary);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-primary) 15%, transparent);
}
-
.field-row {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-xs) var(--space-md);
}
-
.message-row {
align-items: flex-start;
padding-top: var(--space-sm);
padding-bottom: var(--space-sm);
}
-
.field-label {
font-size: 0.8125rem;
font-weight: 600;
@@ -364,28 +364,23 @@
flex-shrink: 0;
min-width: 56px;
}
-
.message-row .field-label {
padding-top: var(--space-xs);
}
-
.field-divider {
height: 1px;
background: var(--border);
margin: 0 var(--space-md);
}
-
.field-input-wrap {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
-
.field-input-wrap.has-error input {
color: var(--error);
}
-
.field-input-wrap input {
flex: 1;
padding: 10px 0;
@@ -439,16 +434,13 @@
width: 18px;
height: 18px;
}
-
.submit-btn:active {
transform: scale(0.92);
}
-
.submit-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
-
.spinner {
width: 16px;
height: 16px;
@@ -457,13 +449,11 @@
border-radius: var(--radius-full);
animation: spin 0.6s linear infinite;
}
-
@keyframes spin {
to {
transform: rotate(360deg);
}
}
-
.platform-blocked {
margin: 0;
font-size: 0.8125rem;
@@ -476,7 +466,6 @@
text-align: center;
padding: var(--space-2xl) var(--space-lg);
}
-
.no-provider-state :global(.no-provider-icon) {
width: 40px;
height: 40px;
@@ -484,7 +473,6 @@
opacity: 0.4;
margin-bottom: var(--space-md);
}
-
.no-provider-title {
font-family: var(--font-display);
font-size: 1rem;
@@ -497,4 +485,16 @@
color: var(--text-muted);
margin: 0;
}
+ .share-hint {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ }
+ .share-hint :global(svg) {
+ flex-shrink: 0;
+ }
diff --git a/src/lib/components/AddVideoModal.svelte b/src/lib/components/AddVideoModal.svelte
index f2a409c..cc9a991 100644
--- a/src/lib/components/AddVideoModal.svelte
+++ b/src/lib/components/AddVideoModal.svelte
@@ -4,8 +4,7 @@
import AddVideo from './AddVideo.svelte';
import UploadStatus from './UploadStatus.svelte';
import BaseSheet from './BaseSheet.svelte';
- import { addToast, toast, toasts } from '$lib/stores/toasts';
- import { clipReadySignal, viewClipSignal } from '$lib/stores/toasts';
+ import { addToast, toasts, clipReadySignal, clipOverlaySignal } from '$lib/stores/toasts';
import { dismissShortcutNudge } from '$lib/stores/shortcutNudge';
import { groupMembers } from '$lib/stores/members';
@@ -14,13 +13,9 @@
let phase = $state<'form' | 'uploading' | 'done' | 'failed'>('form');
let clipId = $state('');
let clipContentType = $state('');
- let caption = $state('');
- let captionDirty = $state(false);
- let serverTitle = $state(null);
let serverArtist = $state(null);
let serverAlbumArt = $state(null);
let pollTimer: ReturnType | null = null;
- let savingCaption = $state(false);
let addVideoRef = $state | null>(null);
let sheetRef = $state | null>(null);
@@ -45,13 +40,9 @@
timers.forEach(clearTimeout);
});
- function handleSubmitted(
- clip: { id: string; status: string; contentType: string },
- submittedCaption: string
- ) {
+ function handleSubmitted(clip: { id: string; status: string; contentType: string }) {
clipId = clip.id;
clipContentType = clip.contentType;
- caption = submittedCaption;
// Remove the processing toast AddVideo created — UploadStatus screen takes over
toasts.update((t) => t.filter((item) => item.clipId !== clip.id));
phase = 'uploading';
@@ -65,10 +56,6 @@
if (!res.ok) return;
const data = await res.json();
- // Update metadata from server (e.g. music title/artist from Odesli)
- if (data.title && !captionDirty) {
- serverTitle = data.title;
- }
if (data.artist) serverArtist = data.artist;
if (data.albumArt) serverAlbumArt = data.albumArt;
@@ -101,24 +88,6 @@
}
}
- async function saveCaption() {
- if (!captionDirty || !caption.trim()) return;
- savingCaption = true;
- try {
- const res = await fetch(`/api/clips/${clipId}`, {
- method: 'PATCH',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ title: caption.trim() })
- });
- if (!res.ok) {
- toast.error('Failed to save caption');
- }
- } catch {
- toast.error('Failed to save caption');
- }
- savingCaption = false;
- }
-
function dismiss() {
// If still uploading, push a background toast
if (phase === 'uploading') {
@@ -134,25 +103,15 @@
}
async function handleSaveAndView() {
- if (captionDirty && caption.trim()) {
- await saveCaption();
- }
clipReadySignal.set(clipId);
- viewClipSignal.set(clipId);
+ clipOverlaySignal.set(clipId);
sheetRef?.dismiss();
}
- function handleCaptionInput(e: Event) {
- caption = (e.target as HTMLInputElement).value;
- captionDirty = true;
- }
-
function handleDismissNudge() {
dismissShortcutNudge();
sheetRef?.dismiss();
}
-
- const displayTitle = $derived(captionDirty ? caption : serverTitle || caption || '');
@@ -178,14 +137,11 @@
{/if}
diff --git a/src/lib/components/ClipOverlay.svelte b/src/lib/components/ClipOverlay.svelte
new file mode 100644
index 0000000..098c31d
--- /dev/null
+++ b/src/lib/components/ClipOverlay.svelte
@@ -0,0 +1,356 @@
+
+
+
+
+
+
+ {#if clip && clip.viewCount > 0}
+ {
+ showViewers = true;
+ }}
+ />
+ {/if}
+
+
+ {#if loading}
+
+ {:else if error || !clip}
+
+
+
Clip not found
+
+
+ {:else}
+
+ {}}
+ oncaptionedit={handleCaptionEdit}
+ ondelete={handleDelete}
+ />
+
+ {/if}
+
+
+{#if showViewers}
+
(showViewers = false)} />
+{/if}
+
+
diff --git a/src/lib/components/CommentInput.svelte b/src/lib/components/CommentInput.svelte
index 7727a46..36ddb8f 100644
--- a/src/lib/components/CommentInput.svelte
+++ b/src/lib/components/CommentInput.svelte
@@ -1,5 +1,6 @@
{#if replyingTo}
@@ -64,13 +84,17 @@
{
text = t;
}}
+ onfocus={handleInputFocus}
onsubmit={() => {
if (canSubmit && !submitting)
onsubmit(text.trim(), attachedGif?.shareUrl || attachedGif?.url);
diff --git a/src/lib/components/CommentsSheet.svelte b/src/lib/components/CommentsSheet.svelte
index 42cfb04..c459abe 100644
--- a/src/lib/components/CommentsSheet.svelte
+++ b/src/lib/components/CommentsSheet.svelte
@@ -76,6 +76,20 @@
loadComments();
});
+ $effect(() => {
+ function handleKeydown(e: KeyboardEvent) {
+ if (e.key === 'Escape') {
+ if (showGifPicker) {
+ showGifPicker = false;
+ } else if (!attachedGif && commentInput?.isEmpty()) {
+ sheetRef?.dismiss();
+ }
+ }
+ }
+ document.addEventListener('keydown', handleKeydown);
+ return () => document.removeEventListener('keydown', handleKeydown);
+ });
+
async function loadComments() {
loading = true;
const result = await fetchComments(clipId);
@@ -234,86 +248,91 @@
sheetId="comments"
{ondismiss}
>
-
-
Loading...
- {:else if feedItems.length === 0} -No comments yet
+Loading...
+ {:else if feedItems.length === 0} +No comments yet
{:else} - {@const comment = item.data} -