v3: linter and formatter pass

This commit is contained in:
khannurien
2026-03-22 16:08:12 +00:00
parent 34e908d1bc
commit a104113e05
17 changed files with 159 additions and 56 deletions

View File

@@ -107,7 +107,9 @@ export async function createUrlDump(
broadcastNewDump(dump); broadcastNewDump(dump);
notifyUserFollowersNewDump(userId, dumpId, title); notifyUserFollowersNewDump(userId, dumpId, title);
} }
if (request.comment) notifyMentions(userId, request.comment, "dump", dumpId, title); if (request.comment) {
notifyMentions(userId, request.comment, "dump", dumpId, title);
}
return dump; return dump;
} }
@@ -335,7 +337,13 @@ export async function updateDump(
if (updatedDump.isPrivate && !dump.isPrivate) broadcastDumpDeleted(dumpId); if (updatedDump.isPrivate && !dump.isPrivate) broadcastDumpDeleted(dumpId);
else if (!updatedDump.isPrivate) broadcastDumpUpdated(updatedDump); else if (!updatedDump.isPrivate) broadcastDumpUpdated(updatedDump);
if (updatedDump.comment) { if (updatedDump.comment) {
notifyMentions(dump.userId, updatedDump.comment, "dump", dumpId, updatedDump.title); notifyMentions(
dump.userId,
updatedDump.comment,
"dump",
dumpId,
updatedDump.title,
);
} }
return updatedDump; return updatedDump;
} }

View File

@@ -207,9 +207,11 @@ export function notifyMentions(
).get(mentionerUserId) as { username: string } | undefined; ).get(mentionerUserId) as { username: string } | undefined;
if (!mentionerRow) return; if (!mentionerRow) return;
const usernames = [...new Set( const usernames = [
[...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase()), ...new Set(
)]; [...body.matchAll(MENTION_RE)].map((m) => m[1].toLowerCase()),
),
];
for (const username of usernames) { for (const username of usernames) {
const mentionedRow = db.prepare( const mentionedRow = db.prepare(

View File

@@ -73,7 +73,9 @@ export function createPlaylist(
isPublic: req.isPublic, isPublic: req.isPublic,
createdAt, createdAt,
}; };
if (req.description) notifyMentions(userId, req.description, "playlist", id, req.title); if (req.description) {
notifyMentions(userId, req.description, "playlist", id, req.title);
}
broadcastPlaylistCreated(playlist); broadcastPlaylistCreated(playlist);
return playlist; return playlist;
} }
@@ -157,7 +159,14 @@ export function updatePlaylist(
const newSlug = makeSlug(newTitle, playlist.id); const newSlug = makeSlug(newTitle, playlist.id);
db.prepare( db.prepare(
`UPDATE playlists SET title = ?, slug = ?, description = ?, is_public = ?, updated_at = ? WHERE id = ?;`, `UPDATE playlists SET title = ?, slug = ?, description = ?, is_public = ?, updated_at = ? WHERE id = ?;`,
).run(newTitle, newSlug, newDescription, newIsPublic ? 1 : 0, now.toISOString(), playlist.id); ).run(
newTitle,
newSlug,
newDescription,
newIsPublic ? 1 : 0,
now.toISOString(),
playlist.id,
);
const updated: Playlist = { const updated: Playlist = {
...playlist, ...playlist,
@@ -167,7 +176,15 @@ export function updatePlaylist(
isPublic: newIsPublic, isPublic: newIsPublic,
updatedAt: now, updatedAt: now,
}; };
if (newDescription) notifyMentions(requestingUserId, newDescription, "playlist", playlist.id, newTitle); if (newDescription) {
notifyMentions(
requestingUserId,
newDescription,
"playlist",
playlist.id,
newTitle,
);
}
broadcastPlaylistUpdated(updated); broadcastPlaylistUpdated(updated);
return updated; return updated;
} }

View File

@@ -2666,8 +2666,12 @@ body.has-player .fab-new {
} }
@keyframes comment-highlight { @keyframes comment-highlight {
0% { background: color-mix(in srgb, var(--color-accent) 18%, transparent); } 0% {
100% { background: transparent; } background: color-mix(in srgb, var(--color-accent) 18%, transparent);
}
100% {
background: transparent;
}
} }
.comment-node--highlight { .comment-node--highlight {
border-radius: 6px; border-radius: 6px;

View File

@@ -161,7 +161,9 @@ function CommentNode({
{children.length > 0 && ( {children.length > 0 && (
<ul <ul
className="comment-replies" className="comment-replies"
style={depth >= MAX_INDENT_DEPTH ? { paddingLeft: 0, marginLeft: 0, borderLeft: "none" } : undefined} style={depth >= MAX_INDENT_DEPTH
? { paddingLeft: 0, marginLeft: 0, borderLeft: "none" }
: undefined}
> >
{children.map((child) => ( {children.map((child) => (
<CommentNode <CommentNode
@@ -229,7 +231,9 @@ function CommentNode({
value={editBody} value={editBody}
onChange={setEditBody} onChange={setEditBody}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleEditSave(e); if (
e.key === "Enter" && (e.ctrlKey || e.metaKey)
) handleEditSave(e);
}} }}
autoResize autoResize
rows={1} rows={1}
@@ -299,7 +303,10 @@ function CommentNode({
<ConfirmModal <ConfirmModal
message="Delete this comment?" message="Delete this comment?"
confirmLabel="Delete" confirmLabel="Delete"
onConfirm={() => { setConfirmDelete(false); handleDelete(); }} onConfirm={() => {
setConfirmDelete(false);
handleDelete();
}}
onCancel={() => setConfirmDelete(false)} onCancel={() => setConfirmDelete(false)}
/> />
)} )}
@@ -312,7 +319,9 @@ function CommentNode({
value={replyBody} value={replyBody}
onChange={setReplyBody} onChange={setReplyBody}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleReply(e); if (
e.key === "Enter" && (e.ctrlKey || e.metaKey)
) handleReply(e);
}} }}
placeholder="Write a reply…" placeholder="Write a reply…"
autoResize autoResize
@@ -348,7 +357,9 @@ function CommentNode({
{children.length > 0 && ( {children.length > 0 && (
<ul <ul
className="comment-replies" className="comment-replies"
style={depth >= MAX_INDENT_DEPTH ? { paddingLeft: 0, marginLeft: 0, borderLeft: "none" } : undefined} style={depth >= MAX_INDENT_DEPTH
? { paddingLeft: 0, marginLeft: 0, borderLeft: "none" }
: undefined}
> >
{children.map((child) => ( {children.map((child) => (
<CommentNode <CommentNode
@@ -440,7 +451,9 @@ export function CommentThread({
value={topLevelBody} value={topLevelBody}
onChange={setTopLevelBody} onChange={setTopLevelBody}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) handleTopLevelSubmit(e); if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
handleTopLevelSubmit(e);
}
}} }}
placeholder="Add a comment…" placeholder="Add a comment…"
autoResize autoResize

View File

@@ -13,7 +13,6 @@ import type {
import { deserializeDump, deserializePlaylistMembership } from "../model.ts"; import { deserializeDump, deserializePlaylistMembership } from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts"; import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts"; import { useWS } from "../hooks/useWS.ts";
import { formatBytes } from "../utils/format.ts";
import { dumpUrl } from "../utils/urls.ts"; import { dumpUrl } from "../utils/urls.ts";
import RichContentCard from "./RichContentCard.tsx"; import RichContentCard from "./RichContentCard.tsx";
import { MediaPlayer } from "./MediaPlayer.tsx"; import { MediaPlayer } from "./MediaPlayer.tsx";

View File

@@ -71,23 +71,21 @@ export function FileDropZone({
<div className="fdz-wrapper"> <div className="fdz-wrapper">
{label && <span className="fdz-label">{label}</span>} {label && <span className="fdz-label">{label}</span>}
<div <div
className={`fdz${dragging ? " fdz--drag" : ""}${disabled ? " fdz--disabled" : ""}${file ? " fdz--filled" : ""}`} className={`fdz${dragging ? " fdz--drag" : ""}${
disabled ? " fdz--disabled" : ""
}${file ? " fdz--filled" : ""}`}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDrop={handleDrop} onDrop={handleDrop}
onClick={file ? undefined : handleClick} onClick={file ? undefined : handleClick}
role={file ? undefined : "button"} role={file ? undefined : "button"}
tabIndex={file || disabled ? undefined : 0} tabIndex={file || disabled ? undefined : 0}
onKeyDown={ onKeyDown={file || disabled ? undefined : (e) => {
file || disabled if (e.key === "Enter" || e.key === " ") {
? undefined e.preventDefault();
: (e) => { handleClick();
if (e.key === "Enter" || e.key === " ") { }
e.preventDefault(); }}
handleClick();
}
}
}
> >
<input <input
ref={inputRef} ref={inputRef}

View File

@@ -1,9 +1,4 @@
import { import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
forwardRef,
useEffect,
useImperativeHandle,
useRef,
} from "react";
import { MentionDropdown } from "./MentionDropdown.tsx"; import { MentionDropdown } from "./MentionDropdown.tsx";
import { useMentionAutocomplete } from "../hooks/useMentionAutocomplete.ts"; import { useMentionAutocomplete } from "../hooks/useMentionAutocomplete.ts";

View File

@@ -28,7 +28,9 @@ export function useDumpListSync(
useEffect(() => { useEffect(() => {
if (deletedDumpIds.size === 0) return; if (deletedDumpIds.size === 0) return;
setDumpsRef.current((prev) => prev.filter((d) => !deletedDumpIds.has(d.id))); setDumpsRef.current((prev) =>
prev.filter((d) => !deletedDumpIds.has(d.id))
);
}, [deletedDumpIds]); }, [deletedDumpIds]);
useEffect(() => { useEffect(() => {

View File

@@ -1,4 +1,10 @@
import { useCallback, useEffect, useRef, useState, type RefObject } from "react"; import {
type RefObject,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import { API_URL } from "../config/api.ts"; import { API_URL } from "../config/api.ts";
export interface UserResult { export interface UserResult {
@@ -70,7 +76,9 @@ export function useMentionAutocomplete(
debounceRef.current = setTimeout(async () => { debounceRef.current = setTimeout(async () => {
try { try {
const res = await fetch( const res = await fetch(
`${API_URL}/api/users/search?q=${encodeURIComponent(mention.query)}`, `${API_URL}/api/users/search?q=${
encodeURIComponent(mention.query)
}`,
); );
const body = await res.json(); const body = await res.json();
if (body.success && body.data.length > 0) { if (body.success && body.data.length > 0) {

View File

@@ -125,7 +125,10 @@ export function Dump() {
if (!el) return; if (!el) return;
el.scrollIntoView({ behavior: "smooth", block: "start" }); el.scrollIntoView({ behavior: "smooth", block: "start" });
el.classList.add("comment-node--highlight"); el.classList.add("comment-node--highlight");
const t = setTimeout(() => el.classList.remove("comment-node--highlight"), 2000); const t = setTimeout(
() => el.classList.remove("comment-node--highlight"),
2000,
);
return () => clearTimeout(t); return () => clearTimeout(t);
}, [comments, location.hash]); }, [comments, location.hash]);

View File

@@ -5,7 +5,6 @@ import { Link, useNavigate } from "react-router";
import { API_URL } from "../config/api.ts"; import { API_URL } from "../config/api.ts";
import type { CreateUrlDumpRequest, RichContent } from "../model.ts"; import type { CreateUrlDumpRequest, RichContent } from "../model.ts";
import { useRequiredAuth } from "../hooks/useAuth.ts"; import { useRequiredAuth } from "../hooks/useAuth.ts";
import { formatBytes } from "../utils/format.ts";
import { dumpUrl } from "../utils/urls.ts"; import { dumpUrl } from "../utils/urls.ts";
import { PageShell } from "../components/PageShell.tsx"; import { PageShell } from "../components/PageShell.tsx";
import RichContentCard from "../components/RichContentCard.tsx"; import RichContentCard from "../components/RichContentCard.tsx";

View File

@@ -97,7 +97,9 @@ function notificationLink(n: Notification): string {
return `/dumps/${(data as DumpUpvotedData).dumpId}`; return `/dumps/${(data as DumpUpvotedData).dumpId}`;
case "user_mentioned": { case "user_mentioned": {
const d = data as UserMentionedData; const d = data as UserMentionedData;
if (d.contextType === "comment") return `/dumps/${d.dumpId}#comment-${d.contextId}`; if (d.contextType === "comment") {
return `/dumps/${d.dumpId}#comment-${d.contextId}`;
}
if (d.contextType === "dump") return `/dumps/${d.contextId}`; if (d.contextType === "dump") return `/dumps/${d.contextId}`;
return `/playlists/${d.contextId}`; return `/playlists/${d.contextId}`;
} }
@@ -109,28 +111,64 @@ function notificationContent(n: Notification): React.ReactNode {
switch (n.type) { switch (n.type) {
case "user_followed": { case "user_followed": {
const d = data as UserFollowedData; const d = data as UserFollowedData;
return <><strong>{d.followerUsername}</strong>{" started following you"}</>; return (
<>
<strong>{d.followerUsername}</strong>
{" started following you"}
</>
);
} }
case "playlist_followed": { case "playlist_followed": {
const d = data as PlaylistFollowedData; const d = data as PlaylistFollowedData;
return <><strong>{d.followerUsername}</strong>{" followed your playlist "}<strong>{d.playlistTitle}</strong></>; return (
<>
<strong>{d.followerUsername}</strong>
{" followed your playlist "}
<strong>{d.playlistTitle}</strong>
</>
);
} }
case "user_dump_posted": { case "user_dump_posted": {
const d = data as UserDumpPostedData; const d = data as UserDumpPostedData;
return <><strong>{d.dumperUsername}</strong>{" posted "}<strong>{d.dumpTitle}</strong></>; return (
<>
<strong>{d.dumperUsername}</strong>
{" posted "}
<strong>{d.dumpTitle}</strong>
</>
);
} }
case "playlist_dump_added": { case "playlist_dump_added": {
const d = data as PlaylistDumpAddedData; const d = data as PlaylistDumpAddedData;
return <><strong>{d.dumpTitle}</strong>{" was added to "}<strong>{d.playlistTitle}</strong></>; return (
<>
<strong>{d.dumpTitle}</strong>
{" was added to "}
<strong>{d.playlistTitle}</strong>
</>
);
} }
case "dump_upvoted": { case "dump_upvoted": {
const d = data as DumpUpvotedData; const d = data as DumpUpvotedData;
return <><strong>{d.voterUsername}</strong>{" upvoted "}<strong>{d.dumpTitle}</strong></>; return (
<>
<strong>{d.voterUsername}</strong>
{" upvoted "}
<strong>{d.dumpTitle}</strong>
</>
);
} }
case "user_mentioned": { case "user_mentioned": {
const d = data as UserMentionedData; const d = data as UserMentionedData;
const where = d.contextTitle || (d.contextType === "comment" ? "a comment" : "a post"); const where = d.contextTitle ||
return <><strong>{d.mentionerUsername}</strong>{" mentioned you in "}<strong>{where}</strong></>; (d.contextType === "comment" ? "a comment" : "a post");
return (
<>
<strong>{d.mentionerUsername}</strong>
{" mentioned you in "}
<strong>{where}</strong>
</>
);
} }
} }
} }

View File

@@ -341,7 +341,9 @@ export function PlaylistDetail() {
dumpMap.set(dump.id, dump); dumpMap.set(dump.id, dump);
const reinserted = order.length > 0 const reinserted = order.length > 0
? [ ? [
...order.filter((id) => dumpMap.has(id)).map((id) => dumpMap.get(id)!), ...order.filter((id) => dumpMap.has(id)).map((id) =>
dumpMap.get(id)!
),
...prev.playlist.dumps.filter((d) => !new Set(order).has(d.id)), ...prev.playlist.dumps.filter((d) => !new Set(order).has(d.id)),
] ]
: [...prev.playlist.dumps, dump]; : [...prev.playlist.dumps, dump];
@@ -684,7 +686,9 @@ export function PlaylistDetail() {
</time> </time>
</Tooltip> </Tooltip>
{playlist.updatedAt && ( {playlist.updatedAt && (
<Tooltip text={`Edited ${playlist.updatedAt.toLocaleString()}`}> <Tooltip
text={`Edited ${playlist.updatedAt.toLocaleString()}`}
>
<span className="playlist-edited-label"> <span className="playlist-edited-label">
edited {relativeTime(playlist.updatedAt)} edited {relativeTime(playlist.updatedAt)}
</span> </span>
@@ -705,7 +709,9 @@ export function PlaylistDetail() {
: ( : (
<div <div
className="playlist-dump-list" className="playlist-dump-list"
onDragOver={isOwner ? (e) => e.preventDefault() : undefined} onDragOver={isOwner
? (e) => e.preventDefault()
: undefined}
> >
{visibleDumps.map((dump) => { {visibleDumps.map((dump) => {
const isActive = activeDumpIds.has(dump.id); const isActive = activeDumpIds.has(dump.id);

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import type { SubmitEvent } from "react"; import type { SubmitEvent } from "react";
import { Link, useNavigate } from "react-router"; import { useNavigate } from "react-router";
import { API_URL } from "../config/api.ts"; import { API_URL } from "../config/api.ts";
import { deserializeAuthResponse } from "../model.ts"; import { deserializeAuthResponse } from "../model.ts";

View File

@@ -77,7 +77,9 @@ export function UserPlaylists() {
const setCreated = useCallback((fn: (prev: Playlist[]) => Playlist[]) => { const setCreated = useCallback((fn: (prev: Playlist[]) => Playlist[]) => {
setState((s) => setState((s) =>
s.status !== "loaded" ? s : { ...s, created: { ...s.created, items: fn(s.created.items) } } s.status !== "loaded"
? s
: { ...s, created: { ...s.created, items: fn(s.created.items) } }
); );
}, []); }, []);
usePlaylistListSync(setCreated, { usePlaylistListSync(setCreated, {

View File

@@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { Link, useNavigate, useParams } from "react-router"; import { Link, useNavigate, useParams } from "react-router";
import { API_URL } from "../config/api.ts"; import { API_URL } from "../config/api.ts";
@@ -151,7 +157,9 @@ export function UserPublicProfile() {
if (next.length < prev.length) { if (next.length < prev.length) {
const nextIds = new Set(next.map((d) => d.id)); const nextIds = new Set(next.map((d) => d.id));
prev.forEach((d, idx) => { prev.forEach((d, idx) => {
if (!nextIds.has(d.id)) removedDumpPositionsRef.current.set(d.id, idx); if (!nextIds.has(d.id)) {
removedDumpPositionsRef.current.set(d.id, idx);
}
}); });
} }
return { ...s, dumps: { ...s.dumps, items: next } }; return { ...s, dumps: { ...s.dumps, items: next } };
@@ -180,7 +188,9 @@ export function UserPublicProfile() {
if (next.length < prev.length) { if (next.length < prev.length) {
const nextIds = new Set(next.map((d) => d.id)); const nextIds = new Set(next.map((d) => d.id));
prev.forEach((d, idx) => { prev.forEach((d, idx) => {
if (!nextIds.has(d.id)) removedVotePositionsRef.current.set(d.id, idx); if (!nextIds.has(d.id)) {
removedVotePositionsRef.current.set(d.id, idx);
}
}); });
} }
return { ...s, votes: { ...s.votes, items: next } }; return { ...s, votes: { ...s.votes, items: next } };
@@ -422,7 +432,6 @@ export function UserPublicProfile() {
} }
}, [lastVoteEvent, me, profileUserId]); }, [lastVoteEvent, me, profileUserId]);
// Save scroll position + loaded state to sessionStorage on scroll // Save scroll position + loaded state to sessionStorage on scroll
useEffect(() => { useEffect(() => {
if (state.status !== "loaded") return; if (state.status !== "loaded") return;