/playlists lays its cards out as tiles, the cover across the top, four to a row and two on a phone, on the wide column: the same PlaylistCard, laid out by its list's class (.playlist-grid), so each theme's card skin carries over. It drops the "public" badge there, every playlist listed being public. On every card, a playlist without a cover shows a tile of its own colour with its initial (ThumbnailPlaceholder) in place of the 📋, and the date is "updated …" of its last dump, or "created …". A middle, ctrl, cmd or shift click anywhere on a playlist card opens it in a new tab — only its title was a link, the rest navigating by a click handler. useCardLink (hooks/shared) is useDumpCardLink's handlers for any card, which the dump cards now get through it. A profile tab's query stays with the tab — going from the playlists to the dumps kept the whole query, so a playlist category, and now ?sort=, landed on the dumps' URL. Only the dump lists pass their category to each other; any other change of tab drops the query. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
3.1 KiB
TypeScript
88 lines
3.1 KiB
TypeScript
/**
|
|
* Decides which of a playlist's dumps a viewer may see. The playlist service
|
|
* filters its listings with it, the WebSocket service filters each client's
|
|
* `playlist_dumps_updated` message the same way, and the queries that list
|
|
* playlists count their dumps by the same rule.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import type { Playlist } from "../../shared/playlist.ts";
|
|
|
|
/**
|
|
* The fields of a playlist's dump that {@link isMemberVisibleTo} needs. A
|
|
* `Dump` has them. For its broadcasts, the playlist service reads only these.
|
|
*/
|
|
export interface PlaylistMember {
|
|
id: string;
|
|
isPrivate: boolean;
|
|
/** The dump's poster, not the playlist's owner. */
|
|
userId: string;
|
|
}
|
|
|
|
/**
|
|
* Checks whether `viewerId` may see `member` in `playlist`. A public dump is
|
|
* visible to all. A private dump is visible only to the playlist's owner, and
|
|
* only if they posted it: moderators don't see it, and neither does the owner
|
|
* once someone else's dump in their playlist goes private. A null or undefined
|
|
* `viewerId` is a signed-out viewer.
|
|
*/
|
|
export function isMemberVisibleTo(
|
|
member: PlaylistMember,
|
|
playlist: Pick<Playlist, "userId">,
|
|
viewerId: string | null | undefined,
|
|
): boolean {
|
|
return !member.isPrivate ||
|
|
(member.userId === viewerId && viewerId === playlist.userId);
|
|
}
|
|
|
|
/**
|
|
* The fields of a playlist's dump that {@link dumpFiguresFor} needs: those
|
|
* that tell who may see it, and when it was added. The playlist service reads
|
|
* them for its broadcasts.
|
|
*/
|
|
export interface DatedPlaylistMember extends PlaylistMember {
|
|
addedAt: Date;
|
|
}
|
|
|
|
/** A figure of the dumps of `playlists p` that the viewer `?1` may see. */
|
|
const visibleDumps = (figure: string) =>
|
|
`(SELECT ${figure} FROM playlist_dumps pd
|
|
INNER JOIN dumps d ON d.id = pd.dump_id
|
|
WHERE pd.playlist_id = p.id
|
|
AND (d.is_private = 0 OR (d.user_id = p.user_id AND p.user_id = ?1))
|
|
)`;
|
|
|
|
/**
|
|
* The `dump_count` and `last_dump_at` columns of a query on `playlists p`:
|
|
* how many of the playlist's dumps the viewer may see, by
|
|
* {@link isMemberVisibleTo}'s rule, and when the latest of them was added,
|
|
* null without any. Both read the viewer's id, or `""` for a signed-out one,
|
|
* as `?1`: it is the query's first parameter, and each `?` after it takes the
|
|
* next.
|
|
*/
|
|
export const VISIBLE_DUMP_FIGURES = `${visibleDumps("COUNT(*)")} as dump_count,
|
|
${visibleDumps("MAX(pd.added_at)")} as last_dump_at`;
|
|
|
|
/**
|
|
* Returns what `playlist` says of its dumps to the viewer, `members` being
|
|
* all of them: the `dumpCount` and `lastDumpAt` that
|
|
* {@link VISIBLE_DUMP_FIGURES} reads in a query. The WebSocket service puts
|
|
* them on the playlist each socket is sent.
|
|
*/
|
|
export function dumpFiguresFor(
|
|
members: DatedPlaylistMember[],
|
|
playlist: Pick<Playlist, "userId">,
|
|
viewerId: string | null | undefined,
|
|
): Required<Pick<Playlist, "dumpCount">> & Pick<Playlist, "lastDumpAt"> {
|
|
const visible = members.filter((m) =>
|
|
isMemberVisibleTo(m, playlist, viewerId)
|
|
);
|
|
return {
|
|
dumpCount: visible.length,
|
|
lastDumpAt: visible.length
|
|
? new Date(Math.max(...visible.map((m) => m.addedAt.getTime())))
|
|
: undefined,
|
|
};
|
|
}
|