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
17 changes: 12 additions & 5 deletions src/lib/components/CommentInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,17 @@
return text.trim().length === 0;
}

function stripEmptyLines(s: string): string {
return s
.split('\n')
.filter((line) => line.trim().length > 0)
.join('\n');
}

function handleSubmit(e: SubmitEvent) {
e.preventDefault();
if (!canSubmit || submitting) return;
onsubmit(text.trim(), attachedGif?.shareUrl || attachedGif?.url);
onsubmit(stripEmptyLines(text.trim()), attachedGif?.shareUrl || attachedGif?.url);
}

function getGifBtnLabel() {
Expand Down Expand Up @@ -102,14 +109,14 @@
maxlength={500}
disabled={submitting}
{members}
singleLine
maxRows={3}
onchange={(t) => {
text = t;
}}
onfocus={handleInputFocus}
onsubmit={() => {
if (canSubmit && !submitting)
onsubmit(text.trim(), attachedGif?.shareUrl || attachedGif?.url);
onsubmit(stripEmptyLines(text.trim()), attachedGif?.shareUrl || attachedGif?.url);
}}
/>
<button type="submit" disabled={!canSubmit || submitting}>Send</button>
Expand Down Expand Up @@ -183,7 +190,7 @@

.input-bar {
display: flex;
align-items: center;
align-items: flex-end;
gap: var(--space-sm);
padding: var(--space-md) var(--space-lg);
border-top: 1px solid var(--border);
Expand All @@ -195,7 +202,7 @@
min-width: 0;
}
.input-bar :global(.mention-input-wrap .input-container) {
border-radius: var(--radius-full);
border-radius: var(--radius-md);
}
.input-bar :global(.mention-input-wrap .overlay-input),
.input-bar :global(.mention-input-wrap .highlight-mirror) {
Expand Down
38 changes: 38 additions & 0 deletions src/lib/components/DoubleExclamationIcon.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
const {
size = 24,
weight = 'bold',
color = 'currentColor'
}: {
size?: number | string;
weight?: string;
color?: string;
} = $props();
</script>

<svg
xmlns="http://www.w3.org/2000/svg"
role="img"
width={size}
height={size}
fill={color}
viewBox="0 0 256 256"
>
<rect width="256" height="256" fill="none" />
{#if weight === 'bold'}
<rect x="80" y="40" width="24" height="120" rx="12" />
<circle cx="92" cy="204" r="20" />
<rect x="152" y="40" width="24" height="120" rx="12" />
<circle cx="164" cy="204" r="20" />
{:else if weight === 'fill'}
<rect x="80" y="40" width="24" height="120" rx="12" />
<circle cx="92" cy="204" r="20" />
<rect x="152" y="40" width="24" height="120" rx="12" />
<circle cx="164" cy="204" r="20" />
{:else}
<rect x="84" y="40" width="16" height="120" rx="8" />
<circle cx="92" cy="200" r="16" />
<rect x="156" y="40" width="16" height="120" rx="8" />
<circle cx="164" cy="200" r="16" />
{/if}
</svg>
19 changes: 16 additions & 3 deletions src/lib/components/GifPicker.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
e.preventDefault();
searchInputEl?.blur();
onclose?.();
} else if (e.key === 'Enter') {
e.preventDefault();
searchInputEl?.blur();
}
}

Expand Down Expand Up @@ -88,6 +91,10 @@
// IntersectionObserver: auto-play visible GIFs, show stills for off-screen
$effect(() => {
if (!gridEl) return;
// Track gifs so this effect re-runs when results change
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reactive dependency
gifs.length;

const io = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
Expand All @@ -101,10 +108,16 @@
{ root: gridEl, rootMargin: '100px 0px' }
);

const imgs = gridEl.querySelectorAll<HTMLImageElement>('img[data-animated]');
imgs.forEach((img) => io.observe(img));
// Delay observation until after Svelte renders new images
const raf = requestAnimationFrame(() => {
const imgs = gridEl!.querySelectorAll<HTMLImageElement>('img[data-animated]');
imgs.forEach((img) => io.observe(img));
});

return () => io.disconnect();
return () => {
cancelAnimationFrame(raf);
io.disconnect();
};
});

async function loadGifs(q?: string) {
Expand Down
21 changes: 19 additions & 2 deletions src/lib/components/MentionInput.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
disabled = false,
members = [],
singleLine = false,
maxRows = 0,
onchange,
onfocus,
onsubmit
Expand All @@ -16,6 +17,7 @@
disabled?: boolean;
members?: GroupMember[];
singleLine?: boolean;
maxRows?: number;
onchange?: (text: string) => void;
onfocus?: () => void;
onsubmit?: () => void;
Expand Down Expand Up @@ -64,6 +66,16 @@
onchange?.(text);
checkForMention();
syncScroll();
if (maxRows > 0) autoResize();
}

function autoResize() {
const el = inputEl as HTMLTextAreaElement | null;
if (!el) return;
el.style.height = 'auto';
const lineHeight = parseFloat(getComputedStyle(el).lineHeight) || 22;
const maxHeight = lineHeight * maxRows;
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
}

function syncScroll() {
Expand Down Expand Up @@ -125,7 +137,8 @@
}
}

if (singleLine && e.key === 'Enter' && !showDropdown) {
if (e.key === 'Enter' && !showDropdown && (singleLine || maxRows > 0)) {
if (e.shiftKey && maxRows > 0) return; // Shift+Enter inserts newline in multi-line mode
e.preventDefault();
onsubmit?.();
}
Expand All @@ -143,6 +156,10 @@
export function clear() {
text = '';
onchange?.('');
if (maxRows > 0) {
const el = inputEl as HTMLTextAreaElement | null;
if (el) el.style.height = 'auto';
}
}

export function getText(): string {
Expand Down Expand Up @@ -216,7 +233,7 @@
{placeholder}
{maxlength}
{disabled}
rows="2"
rows={maxRows > 0 ? 1 : 2}
oninput={handleInput}
onkeydown={handleKeydown}
onclick={handleClick}
Expand Down
9 changes: 7 additions & 2 deletions src/lib/components/ReelItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,8 @@
);
});

const NEGATIVE_EMOJIS = new Set(['👎', '❓']);

function handlePickEmoji(emoji: string) {
showPicker = false;
if (isOwn) return;
Expand All @@ -288,7 +290,7 @@
showerY = pickerY;
showShower = true;
if (!clip.reactions[emoji]?.reacted) onreaction(clip.id, emoji);
if (!clip.favorited) onfavorited(clip.id);
if (!clip.favorited && !NEGATIVE_EMOJIS.has(emoji)) onfavorited(clip.id);
}
function triggerReactionPickerHold(bx: number, by: number) {
if (isOwn) return;
Expand Down Expand Up @@ -369,7 +371,10 @@
{muted}
{uiHidden}
{isOwn}
onsave={() => onfavorited(clip.id)}
onsave={() => {
showPicker = false;
onfavorited(clip.id);
}}
oncomment={() => {
commentsAutoFocus = false;
showComments = true;
Expand Down
8 changes: 4 additions & 4 deletions src/lib/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import type { Component } from 'svelte';
import HeartIcon from 'phosphor-svelte/lib/HeartIcon';
import ThumbsUpIcon from 'phosphor-svelte/lib/ThumbsUpIcon';
import ThumbsDownIcon from 'phosphor-svelte/lib/ThumbsDownIcon';
import SmileyIcon from 'phosphor-svelte/lib/SmileyIcon';
import WarningIcon from 'phosphor-svelte/lib/WarningIcon';
import SmileyXEyesIcon from 'phosphor-svelte/lib/SmileyXEyesIcon';
import DoubleExclamationIcon from '$lib/components/DoubleExclamationIcon.svelte';
import QuestionIcon from 'phosphor-svelte/lib/QuestionIcon';

export interface ReactionDef {
Expand All @@ -21,8 +21,8 @@ export const REACTIONS: ReactionDef[] = [
{ emoji: '❤️', component: HeartIcon, weight: 'fill' },
{ emoji: '👍', component: ThumbsUpIcon, weight: 'regular' },
{ emoji: '👎', component: ThumbsDownIcon, weight: 'regular' },
{ emoji: '😂', component: SmileyIcon, weight: 'regular' },
{ emoji: '‼️', component: WarningIcon, weight: 'bold' },
{ emoji: '😂', component: SmileyXEyesIcon, weight: 'regular' },
{ emoji: '‼️', component: DoubleExclamationIcon, weight: 'bold' },
{ emoji: '❓', component: QuestionIcon, weight: 'regular' }
];

Expand Down
21 changes: 21 additions & 0 deletions src/lib/server/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,27 @@ for (const g of groupsWithoutToken) {
db.update(schema.groups).set({ shortcutToken: uuid() }).where(eq(schema.groups.id, g.id)).run();
}

// Remove favorites that were auto-created by negative-only reactions (👎, ❓)
// A favorite is removed if the user has reactions on that clip but ALL are negative
const NEGATIVE_EMOJIS = ['👎', '❓'];
const negativeFavCleanup = sqlite // eslint-disable-line sonarjs/sql-queries -- static placeholders only
.prepare(
`DELETE FROM favorites WHERE rowid IN (
SELECT f.rowid FROM favorites f
INNER JOIN reactions r ON r.clip_id = f.clip_id AND r.user_id = f.user_id
WHERE NOT EXISTS (
SELECT 1 FROM reactions r2
WHERE r2.clip_id = f.clip_id AND r2.user_id = f.user_id
AND r2.emoji NOT IN (${NEGATIVE_EMOJIS.map(() => '?').join(',')})
)
GROUP BY f.clip_id, f.user_id
)`
)
.run(...NEGATIVE_EMOJIS);
if (negativeFavCleanup.changes > 0) {
log.info({ removed: negativeFavCleanup.changes }, 'Cleaned up negative-reaction-only favorites');
}

// Auto-create the group on first run if the database is empty
const groupCount = db.select({ id: schema.groups.id }).from(schema.groups).limit(1).all();
if (groupCount.length === 0) {
Expand Down
Loading