v4: a playlist's dump count is of the dumps the viewer may see — it counted every dump in the playlist, so a card said "3 dumps" over a page listing two, and told everyone how many private dumps a playlist held. The lists (/playlists, a profile's playlists and followed ones, the search, a dump's playlists, the add-to-playlist panel) now count by isMemberVisibleTo's rule, in SQL as VISIBLE_DUMP_COUNT (lib/playlist-members.ts): the public dumps, and for the owner their own private ones. GET /api/playlists and /api/users/:key/follow/playlists read the optional sign-in for it. The community page and the digests count the public dumps, their figures being the same for every reader. Live, playlist_created and playlist_updated carry each socket's own count: broadcastChange builds the owner's message and the others' apart. playlist_dumps_updated loses its dumpCount, a client counting the dumps it is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
khannurien
2026-10-10 08:25:24 +00:00
parent ffc071af4c
commit 28f910b00e
18 changed files with 273 additions and 94 deletions

View File

@@ -18,6 +18,7 @@ import type { OnlineUser, ServerToClientMessage } from "../../shared/ws.ts";
import type { Playlist } from "../../shared/playlist.ts";
import type { PublicUser, Role } from "../../shared/user.ts";
import {
countVisibleTo,
isMemberVisibleTo,
type PlaylistMember,
} from "../lib/playlist-members.ts";
@@ -179,35 +180,43 @@ export function broadcastPresence(): void {
* `before` is null for a new item, `after` for a deleted one. A private item's
* changes reach its owner's sockets alone. When it goes private, everyone else
* is told it was deleted, and gets nothing of the change; when it goes public,
* they are told it was created, since to them it is new.
* they are told it was created, since to them it is new. The owner's sockets
* and the others' each get a message of their own, built with `toOwner` set
* for the former, for what the two don't see alike.
*/
function broadcastChange<T extends { userId: string }>(
before: T | null,
after: T | null,
isPrivate: (item: T) => boolean,
messages: {
created: (item: T) => ServerToClientMessage;
updated: (item: T) => ServerToClientMessage;
created: (item: T, toOwner: boolean) => ServerToClientMessage;
updated: (item: T, toOwner: boolean) => ServerToClientMessage;
deleted: (item: T) => ServerToClientMessage;
},
): void {
const item = after ?? before;
if (!item) return;
const toOwner = (client: WsClient) => client.userId === item.userId;
const toOthers = (client: WsClient) => client.userId !== item.userId;
const wasPublic = !!before && !isPrivate(before);
const isPublic = !!after && !isPrivate(after);
if (!after) {
broadcast(messages.deleted(item), isPrivate(item) ? toOwner : undefined);
} else if (!before) {
broadcast(messages.created(after), isPrivate(after) ? toOwner : undefined);
} else if (isPrivate(after)) {
broadcast(messages.updated(after), toOwner);
if (!isPrivate(before)) broadcast(messages.deleted(after), toOthers);
} else if (isPrivate(before)) {
broadcast(messages.updated(after), toOwner);
broadcast(messages.created(after), toOthers);
} else {
broadcast(messages.updated(after));
broadcast(
!after
? messages.deleted(item)
: before
? messages.updated(after, true)
: messages.created(after, true),
(client) => client.userId === item.userId,
);
const toOthers = (client: WsClient) => client.userId !== item.userId;
if (after && isPublic) {
broadcast(
wasPublic
? messages.updated(after, false)
: messages.created(after, false),
toOthers,
);
} else if (wasPublic) {
broadcast(messages.deleted(item), toOthers);
}
}
@@ -265,15 +274,32 @@ export function broadcastCommentLikeUpdate(
* Tells the clients that may see a playlist how it changed, as
* {@link broadcastChange} says, for the playlist service after every write to
* its details. A playlist turned private thus leaves the others' views without
* their learning what it became.
* their learning what it became. `members` are the dumps of `after`: each
* socket gets the playlist with the count of those its user may see, the
* owner's private dumps being theirs alone to count.
*/
export function broadcastPlaylistChange(
before: Playlist | null,
after: Playlist | null,
members: PlaylistMember[],
): void {
const counted = (playlist: Playlist, toOwner: boolean): Playlist => ({
...playlist,
dumpCount: countVisibleTo(
members,
playlist,
toOwner ? playlist.userId : null,
),
});
broadcastChange(before, after, (playlist) => !playlist.isPublic, {
created: (playlist) => ({ type: "playlist_created", playlist }),
updated: (playlist) => ({ type: "playlist_updated", playlist }),
created: (playlist, toOwner) => ({
type: "playlist_created",
playlist: counted(playlist, toOwner),
}),
updated: (playlist, toOwner) => ({
type: "playlist_updated",
playlist: counted(playlist, toOwner),
}),
deleted: (p) => ({
type: "playlist_deleted",
playlistId: p.id,
@@ -285,7 +311,8 @@ export function broadcastPlaylistChange(
/**
* Sends `playlist_dumps_updated` to the playlist's audience, each socket with
* only the dumps its user may see, as `GET` would list them. The private dumps
* in a public playlist stay hidden from everyone but their posters.
* in a public playlist stay hidden from everyone but their posters, and so
* does their number: a client counts the dumps it is sent.
*/
export function broadcastPlaylistDumpsUpdated(
playlist: Pick<Playlist, "id" | "isPublic" | "userId">,
@@ -299,7 +326,6 @@ export function broadcastPlaylistDumpsUpdated(
dumpIds: members
.filter((m) => isMemberVisibleTo(m, playlist, client.userId))
.map((m) => m.id),
dumpCount: members.length,
});
}
}