v4: every live update reaches every page that shows what it changed — the client kept each kind of event in one "latest" slot, so two arriving before a render lost the first (two notifications of one dump going public showed one); they now go through a subscription, useWSEvent, and each reaches every subscriber. A playlist made private sent its new title and description to everyone, and kept them on a visitor's page; playlists now follow the dumps' rule, broadcastChange: others get playlist_deleted, and playlist_created when it goes public again. A dump deleted or turned private sends its playlists' dumps again, so their counts and pages follow. Comments on a private dump reach its poster's other tabs. Reading the notifications clears the bell in every tab (notifications_read), and a category change makes every client fetch the categories again (categories_changed). The dump page gives way to a notice when its dump is deleted or made private. A profile gains the user's new dumps, the followed feed those of the users followed and the playlists followed, the search results follow edits and deletions, the index's category feed drops a dump edited out of it, and a renamed user's name follows on cards and comments. After a reconnection, every live list fetches the pages it holds again (usePagedList's resync), and the playlist, the profile, the dump, the categories and the follows are fetched again. The position memory of usePositionAwareSync is now inside the list sync hooks, so a returning item is put back without depending on the order the hooks are called.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,11 @@ import {
|
||||
} from "./rich-content-service.ts";
|
||||
import { isValidHttpUrl } from "../remote/fetch.ts";
|
||||
import { broadcastDumpChange } from "./ws-service.ts";
|
||||
import {
|
||||
broadcastHoldersDumps,
|
||||
type PlaylistHolder,
|
||||
playlistsHolding,
|
||||
} from "./playlist-service.ts";
|
||||
import {
|
||||
notifyMentions,
|
||||
notifyPlaylistFollowersNowPublic,
|
||||
@@ -86,6 +91,23 @@ function assertFileDumpText(title?: string, comment?: string): void {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells clients how a dump changed, for every write of this service:
|
||||
* `broadcastDumpChange` for the dump itself, and, when it was deleted or
|
||||
* changed visibility, the dumps of each playlist holding it, which moved for
|
||||
* their viewers. A deletion passes the `holders` it read before the cascade
|
||||
* took the dump out of them.
|
||||
*/
|
||||
function announceDumpChange(
|
||||
before: Dump | null,
|
||||
after: Dump | null,
|
||||
holders?: PlaylistHolder[],
|
||||
): void {
|
||||
broadcastDumpChange(before, after);
|
||||
if (!before || (after && after.isPrivate === before.isPrivate)) return;
|
||||
broadcastHoldersDumps(holders ?? playlistsHolding(before.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the followers of a dump that went public, `before` being private
|
||||
* and `after` public: the poster's, as if it were just posted, and those of
|
||||
@@ -149,7 +171,7 @@ export async function createUrlDump(
|
||||
});
|
||||
|
||||
const dump: Dump = { ...fetchDump(dumpId), categoryIds };
|
||||
broadcastDumpChange(null, dump);
|
||||
announceDumpChange(null, dump);
|
||||
if (!isPrivate) notifyUserFollowersNewDump(userId, dumpId, title);
|
||||
if (request.comment) {
|
||||
// A private dump's mentions are notified if it goes public.
|
||||
@@ -225,7 +247,7 @@ export async function createFileDump(
|
||||
}
|
||||
|
||||
const dump: Dump = { ...fetchDump(dumpId), categoryIds: dedupedCategoryIds };
|
||||
broadcastDumpChange(null, dump);
|
||||
announceDumpChange(null, dump);
|
||||
if (!isPrivate) notifyUserFollowersNewDump(userId, dumpId, finalTitle);
|
||||
if (comment) {
|
||||
// A private dump's mentions are notified if it goes public.
|
||||
@@ -486,7 +508,7 @@ export async function updateDump(
|
||||
} else {
|
||||
attachCategoryIds("dump", [updatedDump]);
|
||||
}
|
||||
broadcastDumpChange(dump, updatedDump);
|
||||
announceDumpChange(dump, updatedDump);
|
||||
notifyIfWentPublic(dump, updatedDump);
|
||||
if (updatedDump.comment) {
|
||||
if (!updatedDump.isPrivate) {
|
||||
@@ -558,7 +580,7 @@ export async function updateDump(
|
||||
attachCategoryIds("dump", [updatedDump]);
|
||||
}
|
||||
|
||||
broadcastDumpChange(dump, updatedDump);
|
||||
announceDumpChange(dump, updatedDump);
|
||||
notifyIfWentPublic(dump, updatedDump);
|
||||
if (updatedDump.comment) {
|
||||
if (!updatedDump.isPrivate) {
|
||||
@@ -687,7 +709,7 @@ export async function replaceFileDump(
|
||||
isPrivate: finalPrivate,
|
||||
updatedAt: now,
|
||||
};
|
||||
broadcastDumpChange(dump, updatedDump);
|
||||
announceDumpChange(dump, updatedDump);
|
||||
notifyIfWentPublic(dump, updatedDump);
|
||||
return updatedDump;
|
||||
}
|
||||
@@ -799,7 +821,7 @@ export async function refreshDumpMetadata(dumpId: string): Promise<Dump> {
|
||||
`UPDATE dumps SET title = ?, rich_content = ? WHERE id = ?;`,
|
||||
).run(row.title, row.rich_content, row.id);
|
||||
|
||||
broadcastDumpChange(dump, updatedDump);
|
||||
announceDumpChange(dump, updatedDump);
|
||||
|
||||
return updatedDump;
|
||||
}
|
||||
@@ -826,7 +848,7 @@ export function setDumpThumbnail(dumpId: string, mime: string): Dump {
|
||||
dump.id,
|
||||
);
|
||||
const updatedDump: Dump = { ...dump, thumbnailMime: mime };
|
||||
broadcastDumpChange(dump, updatedDump);
|
||||
announceDumpChange(dump, updatedDump);
|
||||
return updatedDump;
|
||||
}
|
||||
|
||||
@@ -842,18 +864,20 @@ export async function removeDumpThumbnail(dumpId: string): Promise<Dump> {
|
||||
.run(dump.id);
|
||||
await Deno.remove(customThumbnailPath(dump.id)).catch(() => {});
|
||||
const updatedDump: Dump = { ...dump, thumbnailMime: undefined };
|
||||
broadcastDumpChange(dump, updatedDump);
|
||||
announceDumpChange(dump, updatedDump);
|
||||
return updatedDump;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a dump with its files and thumbnails, for
|
||||
* `DELETE /api/dumps/:dumpId`, and broadcasts `dump_deleted`.
|
||||
* `DELETE /api/dumps/:dumpId`, and broadcasts `dump_deleted` and the new
|
||||
* dumps of the playlists that held it.
|
||||
*
|
||||
* @throws {APIException} 404 when there is no such dump.
|
||||
*/
|
||||
export async function deleteDump(dumpId: string): Promise<void> {
|
||||
const dump = fetchDump(dumpId);
|
||||
const holders = playlistsHolding(dumpId);
|
||||
|
||||
const result = db.prepare(`DELETE FROM dumps WHERE id = ?;`).run(dumpId);
|
||||
|
||||
@@ -869,5 +893,5 @@ export async function deleteDump(dumpId: string): Promise<void> {
|
||||
await Deno.remove(customThumbnailPath(dump.id)).catch(() => {});
|
||||
}
|
||||
|
||||
broadcastDumpChange(dump, null);
|
||||
announceDumpChange(dump, null, holders);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user