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
54 changes: 51 additions & 3 deletions src/lib/components/ActivitySheet.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@
import { clipOverlaySignal, openCommentsSignal } from '$lib/stores/toasts';
import { createSafeTimeout } from '$lib/safeTimeout';
import BellIcon from 'phosphor-svelte/lib/BellIcon';
import XIcon from 'phosphor-svelte/lib/XIcon';
import { fetchUnwatchedCount } from '$lib/stores/notifications';

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

interface Notification {
id: string;
type: 'reaction' | 'comment' | 'reply' | 'mention';
type: 'reaction' | 'comment' | 'reply' | 'mention' | 'new_clip';
clipId: string;
emoji: string | null;
commentPreview: string | null;
clipContentType: string;
actorUsername: string;
actorAvatar: string | null;
clipThumbnail: string | null;
Expand Down Expand Up @@ -129,13 +132,17 @@
safeTimeout(() => {
ondismiss();
clipOverlaySignal.set(n.clipId);
if (n.type !== 'reaction') {
if (n.type !== 'reaction' && n.type !== 'new_clip') {
openCommentsSignal.set(n.clipId);
}
}, 300);
}

function description(n: Notification): string {
if (n.type === 'new_clip') {
const label = n.clipContentType === 'music' ? 'a song' : 'a video';
return `added ${label}`;
}
if (n.type === 'reaction') {
return `reacted ${n.emoji} to your clip`;
}
Expand All @@ -148,6 +155,15 @@
return 'commented on your clip';
}

async function dismissNotification(e: Event, n: Notification) {
e.preventDefault();
e.stopPropagation();
items = items.filter((item) => item.id !== n.id);
await fetch(`/api/notifications/${n.id}`, { method: 'DELETE' });
fetchUnreadCount();
fetchUnwatchedCount();
}

onDestroy(clearAll);
</script>

Expand All @@ -165,7 +181,7 @@
<BellIcon size={48} />
</div>
<p class="empty-title">No activity yet</p>
<p class="empty-sub">Reactions and comments on your clips will show up here</p>
<p class="empty-sub">New clips, reactions, and comments will show up here</p>
</div>
{:else}
{#each grouped as section (section.label)}
Expand Down Expand Up @@ -204,6 +220,13 @@
<img src="/api/thumbnails/{n.clipThumbnail}" alt="" />
</div>
{/if}
<button
class="dismiss-btn"
onclick={(e) => dismissNotification(e, n)}
aria-label="Dismiss notification"
>
<XIcon size={14} />
</button>
</a>
{/each}
</div>
Expand Down Expand Up @@ -425,6 +448,31 @@
object-fit: cover;
}

.dismiss-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: var(--radius-full);
cursor: pointer;
flex-shrink: 0;
padding: 0;
transition: all 0.15s ease;
}

.dismiss-btn:hover {
background: var(--bg-surface);
color: var(--text-secondary);
}

.dismiss-btn:active {
transform: scale(0.9);
}

.spinner {
display: inline-block;
width: 32px;
Expand Down
3 changes: 3 additions & 0 deletions src/lib/components/ClipOverlay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
const {
clipId,
currentUserId,
isHost = false,
autoScroll,
gifEnabled = false,
openComments = false,
ondismiss
}: {
clipId: string;
currentUserId: string;
isHost?: boolean;
autoScroll: boolean;
gifEnabled?: boolean;
openComments?: boolean;
Expand Down Expand Up @@ -266,6 +268,7 @@
<ReelItem
{clip}
{currentUserId}
{isHost}
active={true}
index={0}
{autoScroll}
Expand Down
82 changes: 33 additions & 49 deletions src/lib/components/CommentInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,8 @@

let text = $state('');
let mentionInputRef = $state<ReturnType<typeof MentionInput> | null>(null);
let inputWrapperHeight = $state(0);

const canSubmit = $derived(text.trim().length > 0 || !!attachedGif);
// Switch from centered to bottom-anchored once the input grows past 1 line.
// Single-line height is ~36px; threshold of 50px gives plenty of buffer.
const isMultiLine = $derived(inputWrapperHeight > 50);

export function focus() {
mentionInputRef?.focus();
Expand Down Expand Up @@ -91,12 +87,7 @@
{/if}

<form class="input-bar" onsubmit={handleSubmit}>
<div
class="input-wrapper"
class:has-gif={gifEnabled}
class:multi-line={isMultiLine}
bind:offsetHeight={inputWrapperHeight}
>
<div class="input-pill" class:focused={false}>
<MentionInput
bind:this={mentionInputRef}
placeholder={replyingTo ? `Reply to ${replyingTo.username}...` : 'Add a comment...'}
Expand Down Expand Up @@ -213,60 +204,53 @@
padding-bottom: max(var(--space-md), env(safe-area-inset-bottom));
}

.input-wrapper {
position: relative;
/* The pill is the visible rounded container — holds both the textarea and the action icons. */
.input-pill {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
transition: border-color 0.2s ease;
}
.input-wrapper :global(.mention-input-wrap) {
width: 100%;
.input-pill.focused {
border-color: var(--accent-primary);
}
.input-wrapper :global(.input-container) {
border-radius: var(--radius-md);

/* Strip visual chrome from MentionInput's container — the pill handles it now. */
.input-pill :global(.mention-input-wrap) {
flex: 1;
min-width: 0;
}
.input-pill :global(.input-container) {
background: transparent;
border: none;
border-radius: 0;
}
/* Vertical padding sets the single-line height; reserve right space for send icon.
* padding-top is larger than padding-bottom to optically center the text — glyphs
* sit above the geometric midpoint of their line box, so extra top padding shifts
* the perceived text center down to match the vertically-centered icons. */
.input-wrapper :global(.overlay-input),
.input-wrapper :global(.highlight-mirror) {
.input-pill :global(.input-container.focused) {
border-color: transparent;
}
.input-pill :global(.overlay-input),
.input-pill :global(.highlight-mirror) {
font-size: 1rem;
padding-top: 11px;
padding-bottom: 7px;
padding-right: 38px;
padding: 12px var(--space-md);
}
/*
* Pin the initial textarea height to exactly 1 line so UA stylesheets
* can't inflate it and push icons below the text. autoResize() overrides
* this via inline style once the user starts typing.
*/
.input-wrapper :global(textarea.overlay-input) {
height: calc(1.4em + 18px);
/* Strip macOS/iOS native appearance so our height rule takes full control */
.input-pill :global(textarea.overlay-input) {
height: calc(1.4em + 24px);
-webkit-appearance: none;
appearance: none;
}
/* More right space when GIF pill is also showing */
.input-wrapper.has-gif :global(.overlay-input),
.input-wrapper.has-gif :global(.highlight-mirror) {
padding-right: 70px;
}

/* Actions row: centered vertically on single-line, bottom-anchored as input grows. */
/* Action buttons sit to the right of the textarea, vertically centered by the flex parent. */
.input-actions {
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 8px;
display: flex;
align-items: center;
align-self: center;
gap: 4px;
z-index: 10;
}
.input-wrapper.multi-line .input-actions {
top: auto;
bottom: 7px;
transform: none;
padding-right: 8px;
flex-shrink: 0;
}

.gif-pill {
Expand Down
8 changes: 5 additions & 3 deletions src/lib/components/FilterBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,11 @@
opacity: 0;
}

.filter-bar.dimmed {
opacity: 0.25;
pointer-events: none;
@media (max-width: 549px) {
.filter-bar.dimmed {
opacity: 0;
pointer-events: none;
}
}

.filter-bar.pull-snapping {
Expand Down
10 changes: 8 additions & 2 deletions src/lib/components/MentionInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,11 @@
const el = inputEl as HTMLTextAreaElement | null;
if (!el) return;
el.style.height = 'auto';
const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || 22;
const maxHeight = lineHeight * maxRows;
const styles = getComputedStyle(el);
const lineHeight = parseFloat(styles.lineHeight) || 22;
const paddingTop = parseFloat(styles.paddingTop) || 0;
const paddingBottom = parseFloat(styles.paddingBottom) || 0;
const maxHeight = lineHeight * maxRows + paddingTop + paddingBottom;
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
}

Expand Down Expand Up @@ -298,9 +301,11 @@
}

.overlay-input {
display: block;
position: relative;
z-index: 2;
width: 100%;
margin: 0;
padding: var(--space-sm) var(--space-md);
border: none;
background: transparent;
Expand All @@ -323,6 +328,7 @@
textarea.overlay-input {
resize: none;
line-height: 1.4;
margin: 0;
}

.mention-dropdown {
Expand Down
Loading