v3: code quality pass, various bug fixes

This commit is contained in:
khannurien
2026-03-23 07:47:49 +00:00
parent d94a319d96
commit fbbbb43258
44 changed files with 1060 additions and 698 deletions

View File

@@ -7,7 +7,7 @@ import {
} from "react";
import { Link, useParams } from "react-router";
import { API_URL } from "../config/api.ts";
import { API_URL, DEFAULT_PAGE_SIZE } from "../config/api.ts";
import { friendlyFetchError } from "../utils/apiError.ts";
import type {
PaginatedData,
@@ -15,9 +15,11 @@ import type {
PublicUser,
RawPlaylist,
} from "../model.ts";
import { deserializePlaylist, deserializePublicUser } from "../model.ts";
import { deserializePlaylist, deserializePublicUser, hydratePlaylist } from "../model.ts";
import { useAuth } from "../hooks/useAuth.ts";
import { useWS } from "../hooks/useWS.ts";
import { usePlaylistListSync } from "../hooks/usePlaylistListSync.ts";
import { usePositionAwareSync } from "../hooks/usePositionAwareSync.ts";
import { useInfiniteScroll } from "../hooks/useInfiniteScroll.ts";
import { useFeedCache } from "../hooks/useFeedCache.ts";
import { Avatar } from "../components/Avatar.tsx";
@@ -27,10 +29,6 @@ import { ConfirmModal } from "../components/ConfirmModal.tsx";
import { PageShell } from "../components/PageShell.tsx";
import { PageError } from "../components/PageError.tsx";
const PAGE_SIZE = 20;
const hydratePlaylist = (raw: Playlist): Playlist =>
deserializePlaylist(raw as unknown as RawPlaylist);
interface PlaylistFeed {
items: Playlist[];
hasMore: boolean;
@@ -55,6 +53,7 @@ function initialFeed(items: Playlist[], hasMore: boolean): PlaylistFeed {
export function UserPlaylists() {
const { username } = useParams();
const { user: me, authFetch, token } = useAuth();
const { lastPlaylistEvent } = useWS();
const { cached: cachedCreated, saveState: saveCreated } = useFeedCache<
Playlist
@@ -82,9 +81,21 @@ export function UserPlaylists() {
: { ...s, created: { ...s.created, items: fn(s.created.items) } }
);
}, []);
const createdItems = state.status === "loaded" ? state.created.items : [];
const lastPlaylistItem = lastPlaylistEvent?.type === "updated"
? (lastPlaylistEvent.playlist ?? null)
: null;
usePositionAwareSync(
createdItems,
setCreated,
lastPlaylistItem,
(p) => !p.isPublic,
(p) => p.isPublic && p.userId === profileUserId,
);
usePlaylistListSync(setCreated, {
isOwner: isOwnProfile,
ownerId: profileUserId ?? undefined,
skipReinsert: true,
});
const setFollowed = useCallback((fn: (prev: Playlist[]) => Playlist[]) => {
@@ -99,13 +110,14 @@ export function UserPlaylists() {
useEffect(() => {
if (!username) return;
setState({ status: "loading" });
const controller = new AbortController();
const authHeaders: HeadersInit = token
? { Authorization: `Bearer ${token}` }
: {};
if (cachedCreated && cachedFollowed) {
fetch(`${API_URL}/api/users/${username}`)
fetch(`${API_URL}/api/users/${username}`, { signal: controller.signal })
.then((r) => r.json())
.then((body) => {
if (!body.success) throw new Error("User not found");
@@ -126,23 +138,22 @@ export function UserPlaylists() {
},
});
})
.catch((err) =>
setState({
status: "error",
error: friendlyFetchError(err),
})
);
return;
.catch((err) => {
if (err.name === "AbortError") return;
setState({ status: "error", error: friendlyFetchError(err) });
});
return () => controller.abort();
}
Promise.all([
fetch(`${API_URL}/api/users/${username}`),
fetch(`${API_URL}/api/users/${username}`, { signal: controller.signal }),
fetch(
`${API_URL}/api/users/${username}/playlists?page=1&limit=${PAGE_SIZE}`,
{ headers: authHeaders },
`${API_URL}/api/users/${username}/playlists?page=1&limit=${DEFAULT_PAGE_SIZE}`,
{ headers: authHeaders, signal: controller.signal },
),
fetch(
`${API_URL}/api/users/${username}/followed-playlists?page=1&limit=${PAGE_SIZE}`,
`${API_URL}/api/users/${username}/followed-playlists?page=1&limit=${DEFAULT_PAGE_SIZE}`,
{ signal: controller.signal },
),
])
.then(([userRes, createdRes, followedRes]) =>
@@ -169,12 +180,11 @@ export function UserPlaylists() {
),
});
})
.catch((err) =>
setState({
status: "error",
error: friendlyFetchError(err),
})
);
.catch((err) => {
if (err.name === "AbortError") return;
setState({ status: "error", error: friendlyFetchError(err) });
});
return () => controller.abort();
}, [username]);
const loadMoreCreated = useCallback(() => {
@@ -189,7 +199,7 @@ export function UserPlaylists() {
: s
);
fetch(
`${API_URL}/api/users/${username}/playlists?page=${nextPage}&limit=${PAGE_SIZE}`,
`${API_URL}/api/users/${username}/playlists?page=${nextPage}&limit=${DEFAULT_PAGE_SIZE}`,
{ headers: token ? { Authorization: `Bearer ${token}` } : {} },
)
.then((r) => r.json())
@@ -230,7 +240,7 @@ export function UserPlaylists() {
: s
);
fetch(
`${API_URL}/api/users/${username}/followed-playlists?page=${nextPage}&limit=${PAGE_SIZE}`,
`${API_URL}/api/users/${username}/followed-playlists?page=${nextPage}&limit=${DEFAULT_PAGE_SIZE}`,
)
.then((r) => r.json())
.then((body) => {