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:
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Spares every page fetching the site's categories: it loads both kinds once
|
||||
* for the whole app, and again when the category manager changes them. `App`
|
||||
* mounts it.
|
||||
* for the whole app, and again whenever the category manager changes them,
|
||||
* here or in another browser, and after the socket reconnects. `App` mounts
|
||||
* it inside `WSProvider`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -22,6 +23,7 @@ import type { Category } from "../../../shared/category.ts";
|
||||
import { deserializeCategory } from "../../utils/shared/deserialize.ts";
|
||||
import type { Wire } from "../../../shared/api.ts";
|
||||
import { expectOk } from "../../utils/shared/api.ts";
|
||||
import { useOnReconnect, useWSEvent } from "../../hooks/ws/useWS.ts";
|
||||
|
||||
// A failed first fetch waits this long before it is tried again.
|
||||
const RETRY_MS = 10_000;
|
||||
@@ -81,6 +83,10 @@ export function CategoriesProvider(
|
||||
}, [load, attempt]);
|
||||
|
||||
const refresh = useCallback(() => load().catch(() => {}), [load]);
|
||||
// `categories_changed` reaches the manager's own tab too, which refreshed
|
||||
// already: one fetch more.
|
||||
useWSEvent("categories", refresh);
|
||||
useOnReconnect(refresh);
|
||||
|
||||
const value: CategoriesContextValue = useMemo(
|
||||
() => ({ categories, isLoaded, refresh }),
|
||||
|
||||
@@ -29,14 +29,16 @@ const RETRY_MS = 10_000;
|
||||
|
||||
/**
|
||||
* Provides {@link FollowContext}. It fetches the follows whenever the token
|
||||
* changes, and again when `follows_changed` says another tab changed them. It
|
||||
* empties them on sign-out.
|
||||
* changes, again when `follows_changed` says another tab changed them, and
|
||||
* after the socket reconnects. It empties them on sign-out.
|
||||
*/
|
||||
export function FollowProvider(
|
||||
{ children }: FollowProviderProps,
|
||||
): React.JSX.Element {
|
||||
const { token, authFetch } = useAuth();
|
||||
const { followsVersion } = useWS();
|
||||
// A reconnection fetches them again too: a `follows_changed` may have been
|
||||
// missed while the socket was down.
|
||||
const { followsVersion, connectionEpoch } = useWS();
|
||||
const [followedUserIds, setFollowedUserIds] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
@@ -82,7 +84,7 @@ export function FollowProvider(
|
||||
controller.abort();
|
||||
clearTimeout(retry);
|
||||
};
|
||||
}, [token, followsVersion, attempt]);
|
||||
}, [token, followsVersion, connectionEpoch, attempt]);
|
||||
|
||||
/**
|
||||
* Adds or removes `id` in a set at once, sends the request, and undoes the
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Live updates from the server reach the pages through the three contexts
|
||||
* declared here: votes and likes, dumps, playlists, comments, profiles and
|
||||
* notifications in one, the chat in another, who is online in the third. They
|
||||
* are split so that a feed doesn't re-render with every chat message or
|
||||
* presence change. `WSProvider` fills them from its WebSocket, and components
|
||||
* read them with `useWS`, `useWSChat` and `useWSPresence`.
|
||||
* Live updates from the server reach the pages through the contexts declared
|
||||
* here. Three hold state: votes, likes, deletions and counts in one, the chat
|
||||
* in another, who is online in the third, split so that a feed doesn't
|
||||
* re-render with every chat message or presence change. The fourth passes on
|
||||
* events, each change to a dump, playlist, comment or profile and each
|
||||
* notification, to the components that subscribe to their kind. `WSProvider`
|
||||
* fills them all from its WebSocket, and components read them with `useWS`,
|
||||
* `useWSChat`, `useWSPresence` and `useWSEvent`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -20,7 +22,7 @@ import type { PublicUser } from "../../../shared/user.ts";
|
||||
|
||||
/**
|
||||
* Keeps a user's upvoted list live, on their profile's overview and tab.
|
||||
* `WSProvider` sets it as `lastVoteEvent` on each `votes_update`, and
|
||||
* `WSProvider` emits it as a `vote` event on each `votes_update`, and
|
||||
* `useUpvotedList` adds or drops the dump.
|
||||
*/
|
||||
export interface VoteEvent {
|
||||
@@ -31,7 +33,7 @@ export interface VoteEvent {
|
||||
|
||||
/**
|
||||
* Keeps the playlists on screen up to date, in lists, on a playlist's page and
|
||||
* on a dump's page. `WSProvider` sets it as `lastPlaylistEvent` on each
|
||||
* on a dump's page. `WSProvider` emits it as a `playlist` event on each
|
||||
* `playlist_*` message; lists apply it through `usePlaylistListSync`, a
|
||||
* playlist's page through `usePlaylist`. Which fields are set depends on
|
||||
* `type`.
|
||||
@@ -53,9 +55,9 @@ export interface PlaylistEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps comment threads and comment counts live. `WSProvider` sets it as
|
||||
* `lastCommentEvent` on each `comment_*` message; the `Dump` page applies it
|
||||
* to its thread, and `useDumpListSync` to the counts on dump cards.
|
||||
* Keeps comment threads and comment counts live. `WSProvider` emits it as a
|
||||
* `comment` event on each `comment_*` message; the `Dump` page applies it to
|
||||
* its thread, and `useDumpListSync` to the counts on dump cards.
|
||||
*/
|
||||
export interface CommentEvent {
|
||||
type: "created" | "deleted" | "updated";
|
||||
@@ -67,14 +69,76 @@ export interface CommentEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a profile page live while its user edits their profile. `WSProvider`
|
||||
* sets it as `lastUserEvent` on each `user_updated`, which the server sends on
|
||||
* any change to a profile, and `useProfile` applies it.
|
||||
* Keeps every dump on screen up to date. `WSProvider` emits it as a `dump`
|
||||
* event on each `dump_created` and `dump_updated`; `useDumpListSync` applies
|
||||
* it to lists, and the `Dump` page to its dump. A dump that turns public
|
||||
* comes `created` to those who couldn't see it.
|
||||
*/
|
||||
export interface DumpEvent {
|
||||
type: "created" | "updated";
|
||||
dump: Dump;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps a user's profile, and their name on what they posted, up to date.
|
||||
* `WSProvider` emits it as a `user` event on each `user_updated`, which the
|
||||
* server sends on any change to a profile; `useProfile` applies it to the
|
||||
* profile page, and the list sync hooks to the names on cards.
|
||||
*/
|
||||
export interface UserEvent {
|
||||
user: PublicUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* The events `WSProvider` passes on, by kind, to the components that
|
||||
* subscribe with `useWSEvent`. Unlike the state of `WSContext`, every event
|
||||
* reaches every subscriber, however many arrive before the next render.
|
||||
* `categories` says the categories changed, and carries nothing.
|
||||
*/
|
||||
export interface WSEvents {
|
||||
vote: VoteEvent;
|
||||
dump: DumpEvent;
|
||||
playlist: PlaylistEvent;
|
||||
comment: CommentEvent;
|
||||
user: UserEvent;
|
||||
notification: Notification;
|
||||
categories: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivers {@link WSEvents} to their subscribers. `WSProvider` creates one
|
||||
* with {@link createWSEventBus} and emits on it as messages arrive; components
|
||||
* subscribe through {@link WSEventsContext}, and tests emit on their own.
|
||||
*/
|
||||
export interface WSEventBus {
|
||||
/** Calls `handler` with each event of `kind`, until the returned call. */
|
||||
subscribe: <K extends keyof WSEvents>(
|
||||
kind: K,
|
||||
handler: (event: WSEvents[K]) => void,
|
||||
) => () => void;
|
||||
/** Calls the handlers of `kind` now, in the order they subscribed. */
|
||||
emit: <K extends keyof WSEvents>(kind: K, event: WSEvents[K]) => void;
|
||||
}
|
||||
|
||||
/** Returns a bus with no subscribers. */
|
||||
export function createWSEventBus(): WSEventBus {
|
||||
const handlers = new Map<keyof WSEvents, Set<(event: never) => void>>();
|
||||
return {
|
||||
subscribe(kind, handler) {
|
||||
let set = handlers.get(kind);
|
||||
if (!set) handlers.set(kind, set = new Set());
|
||||
set.add(handler);
|
||||
return () => set.delete(handler);
|
||||
},
|
||||
emit(kind, event) {
|
||||
// A copy: a handler may unsubscribe another, as a navigation does.
|
||||
for (const handler of [...handlers.get(kind) ?? []]) {
|
||||
(handler as (event: WSEvents[typeof kind]) => void)(event);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quotes the message a chat message replies to. `ChatModal` shows it in the
|
||||
* reply bar once the user picks it, and it stays on the pending message so
|
||||
@@ -120,12 +184,11 @@ export interface ChatError {
|
||||
}
|
||||
|
||||
/**
|
||||
* Carries everything live but the chat and presence, for pages to lay over
|
||||
* Carries the live state but the chat and presence, for pages to lay over
|
||||
* what they fetched. `WSProvider` provides it through {@link WSContext}, and
|
||||
* components read it with `useWS`. Each `last*Event` field holds only the
|
||||
* latest event of its kind: a consumer reacts in an effect when one changes,
|
||||
* and an event that another replaces within the same render is missed. After a
|
||||
* reconnect, `connectionEpoch` tells pages to fetch what they missed.
|
||||
* components read it with `useWS`. The changes to single items come as events
|
||||
* instead, through {@link WSEventsContext}. After a reconnect,
|
||||
* `connectionEpoch` tells pages to fetch what they missed.
|
||||
*/
|
||||
export interface WSContextValue {
|
||||
wsStatus: "connecting" | "connected" | "disconnected";
|
||||
@@ -159,27 +222,16 @@ export interface WSContextValue {
|
||||
* it.
|
||||
*/
|
||||
deletedDumpIds: Set<string>;
|
||||
lastVoteEvent: VoteEvent | null;
|
||||
/**
|
||||
* Keeps a dump up to date in every list that shows it. It holds the dump as
|
||||
* its latest `dump_created` or `dump_updated` has it, and a list updates it
|
||||
* in place, or puts it back where it was.
|
||||
* Takes deleted playlists off every list at once, as `deletedDumpIds` does
|
||||
* dumps.
|
||||
*/
|
||||
lastDumpEvent: Dump | null;
|
||||
lastPlaylistEvent: PlaylistEvent | null;
|
||||
deletedPlaylistIds: Set<string>;
|
||||
lastCommentEvent: CommentEvent | null;
|
||||
lastUserEvent: UserEvent | null;
|
||||
/**
|
||||
* Drives the bell's badge. It comes from the `welcome`, then each new
|
||||
* notification counts it up.
|
||||
* notification counts it up, and `notifications_read` zeroes it.
|
||||
*/
|
||||
unreadNotificationCount: number;
|
||||
/**
|
||||
* Makes the bell ring, and puts the notification on the notifications page
|
||||
* as it arrives.
|
||||
*/
|
||||
lastNotification: Notification | null;
|
||||
/**
|
||||
* Tells pages to fetch again what they missed while the socket was down. It
|
||||
* goes up on each reconnect, never on the first connection.
|
||||
@@ -318,14 +370,8 @@ export const defaultWSValue: WSContextValue = {
|
||||
myLikes: new Set(),
|
||||
recentDumps: [],
|
||||
deletedDumpIds: new Set(),
|
||||
lastVoteEvent: null,
|
||||
lastDumpEvent: null,
|
||||
lastPlaylistEvent: null,
|
||||
deletedPlaylistIds: new Set(),
|
||||
lastCommentEvent: null,
|
||||
lastUserEvent: null,
|
||||
unreadNotificationCount: 0,
|
||||
lastNotification: null,
|
||||
connectionEpoch: 0,
|
||||
followsVersion: 0,
|
||||
castVote: () => {},
|
||||
@@ -382,3 +428,11 @@ export const WSPresenceContext: React.Context<WSPresenceContextValue> =
|
||||
>(
|
||||
defaultWSPresenceValue,
|
||||
);
|
||||
|
||||
/**
|
||||
* Carries the bus `WSProvider` emits its events on, for `useWSEvent`. Outside
|
||||
* `WSProvider`, it is a bus no one emits on.
|
||||
*/
|
||||
export const WSEventsContext: React.Context<
|
||||
Pick<WSEventBus, "subscribe">
|
||||
> = createContext<Pick<WSEventBus, "subscribe">>(createWSEventBus());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Brings the server's live updates into the app. It holds the one WebSocket to
|
||||
* `/ws` and turns its messages into the state of `WSContext`, `WSChatContext`,
|
||||
* `WSPresenceContext` and `AvatarOverrideContext`. `App` mounts it around every
|
||||
* page. The protocol is `shared/ws.ts`, and the server's side is
|
||||
* `api/routes/ws.ts`.
|
||||
* `WSPresenceContext` and `AvatarOverrideContext`, and into the events of
|
||||
* `WSEventsContext`. `App` mounts it around every page. The protocol is
|
||||
* `shared/ws.ts`, and the server's side is `api/routes/ws.ts`.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -20,15 +20,13 @@ import {
|
||||
import {
|
||||
type ChatError,
|
||||
type ChatReplyTarget,
|
||||
type CommentEvent,
|
||||
createWSEventBus,
|
||||
type PendingChatMessage,
|
||||
type PlaylistEvent,
|
||||
type UserEvent,
|
||||
type VoteEvent,
|
||||
WSChatContext,
|
||||
type WSChatContextValue,
|
||||
WSContext,
|
||||
type WSContextValue,
|
||||
WSEventsContext,
|
||||
WSPresenceContext,
|
||||
type WSPresenceContextValue,
|
||||
} from "./WSContext.ts";
|
||||
@@ -45,7 +43,6 @@ import type {
|
||||
OnlineUser,
|
||||
ServerToClientMessage,
|
||||
} from "../../../shared/ws.ts";
|
||||
import type { Notification } from "../../../shared/notification.ts";
|
||||
import {
|
||||
deserializeChatMessage,
|
||||
deserializeComment,
|
||||
@@ -215,9 +212,10 @@ function parseWSMessage(data: string): Wire<ServerToClientMessage> | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides {@link WSContext}, {@link WSChatContext}, {@link WSPresenceContext}
|
||||
* and {@link AvatarOverrideContext}. It connects with the session's token, or
|
||||
* anonymously, and reconnects with a backoff doubling up to 30 s. A new token
|
||||
* Provides {@link WSContext}, {@link WSChatContext}, {@link WSPresenceContext},
|
||||
* {@link AvatarOverrideContext} and {@link WSEventsContext}. It connects with
|
||||
* the session's token, or anonymously, and reconnects with a backoff doubling
|
||||
* up to 30 s. A new token
|
||||
* (sign-in, sign-out, another account) clears the live state and reconnects.
|
||||
* A new notification or chat message chimes, and the unread counts go in the
|
||||
* tab title.
|
||||
@@ -240,22 +238,12 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
const [myLikes, setMyLikes] = useState<Set<string>>(new Set());
|
||||
const [recentDumps, setRecentDumps] = useState<Dump[]>([]);
|
||||
const [deletedDumpIds, setDeletedDumpIds] = useState<Set<string>>(new Set());
|
||||
const [lastVoteEvent, setLastVoteEvent] = useState<VoteEvent | null>(null);
|
||||
const [lastDumpEvent, setLastDumpEvent] = useState<Dump | null>(null);
|
||||
const [lastPlaylistEvent, setLastPlaylistEvent] = useState<
|
||||
PlaylistEvent | null
|
||||
>(null);
|
||||
const [deletedPlaylistIds, setDeletedPlaylistIds] = useState<Set<string>>(
|
||||
new Set(),
|
||||
);
|
||||
const [lastCommentEvent, setLastCommentEvent] = useState<CommentEvent | null>(
|
||||
null,
|
||||
);
|
||||
const [lastUserEvent, setLastUserEvent] = useState<UserEvent | null>(null);
|
||||
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0);
|
||||
const [lastNotification, setLastNotification] = useState<Notification | null>(
|
||||
null,
|
||||
);
|
||||
// Never replaced: the subscribers keep it for as long as they are mounted.
|
||||
const [events] = useState(createWSEventBus);
|
||||
const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
|
||||
const [deletedChatIds, setDeletedChatIds] = useState<Set<string>>(new Set());
|
||||
const [unreadChatCount, setUnreadChatCount] = useState(0);
|
||||
@@ -287,14 +275,8 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
setMyLikes(new Set());
|
||||
setRecentDumps([]);
|
||||
setDeletedDumpIds(new Set());
|
||||
setLastVoteEvent(null);
|
||||
setLastDumpEvent(null);
|
||||
setLastPlaylistEvent(null);
|
||||
setDeletedPlaylistIds(new Set());
|
||||
setLastCommentEvent(null);
|
||||
setLastUserEvent(null);
|
||||
setUnreadNotificationCount(0);
|
||||
setLastNotification(null);
|
||||
setChatMessages([]);
|
||||
setDeletedChatIds(new Set());
|
||||
setUnreadChatCount(0);
|
||||
@@ -480,7 +462,7 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
|
||||
case "votes_update": {
|
||||
const { dumpId, voteCount, voterId, action } = msg;
|
||||
setLastVoteEvent({ dumpId, voterId, action });
|
||||
events.emit("vote", { dumpId, voterId, action });
|
||||
// While this tab has votes on it in flight, their acks settle it.
|
||||
if (votes.pending(dumpId)) break;
|
||||
setVoteCounts((prev) => ({ ...prev, [dumpId]: voteCount }));
|
||||
@@ -531,13 +513,13 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
)
|
||||
);
|
||||
// The profiles put a dump back where it was when it turns public.
|
||||
setLastDumpEvent(dump);
|
||||
events.emit("dump", { type: "created", dump });
|
||||
break;
|
||||
}
|
||||
|
||||
case "dump_updated": {
|
||||
const dump = deserializeDump(msg.dump);
|
||||
setLastDumpEvent(dump);
|
||||
events.emit("dump", { type: "updated", dump });
|
||||
// An edit is not an arrival: it must not pull an old dump to the
|
||||
// top of the feeds.
|
||||
setRecentDumps((prev) =>
|
||||
@@ -586,7 +568,17 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
case "playlist_created":
|
||||
case "playlist_updated": {
|
||||
const playlist = deserializePlaylist(msg.playlist);
|
||||
setLastPlaylistEvent({
|
||||
// Also sent when a playlist goes public, which may undo its
|
||||
// `playlist_deleted` from going private.
|
||||
if (msg.type === "playlist_created") {
|
||||
setDeletedPlaylistIds((prev) => {
|
||||
if (!prev.has(playlist.id)) return prev;
|
||||
const next = new Set(prev);
|
||||
next.delete(playlist.id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
events.emit("playlist", {
|
||||
type: msg.type === "playlist_created" ? "created" : "updated",
|
||||
playlistId: playlist.id,
|
||||
playlist,
|
||||
@@ -596,14 +588,15 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
|
||||
case "playlist_deleted": {
|
||||
const { playlistId, userId } = msg;
|
||||
// Also sent to others when someone's playlist goes private.
|
||||
setDeletedPlaylistIds((prev) => new Set([...prev, playlistId]));
|
||||
setLastPlaylistEvent({ type: "deleted", playlistId, userId });
|
||||
events.emit("playlist", { type: "deleted", playlistId, userId });
|
||||
break;
|
||||
}
|
||||
|
||||
case "playlist_dumps_updated": {
|
||||
const { playlistId, dumpIds, dumpCount } = msg;
|
||||
setLastPlaylistEvent({
|
||||
events.emit("playlist", {
|
||||
type: "dumps_updated",
|
||||
playlistId,
|
||||
dumpIds,
|
||||
@@ -614,7 +607,7 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
|
||||
case "user_updated": {
|
||||
const user = deserializePublicUser(msg.user);
|
||||
setLastUserEvent({ user });
|
||||
events.emit("user", { user });
|
||||
setAvatarOverrides((prev) => ({
|
||||
...prev,
|
||||
[user.id]: {
|
||||
@@ -627,7 +620,7 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
|
||||
case "comment_created": {
|
||||
const comment = deserializeComment(msg.comment);
|
||||
setLastCommentEvent({
|
||||
events.emit("comment", {
|
||||
type: "created",
|
||||
dumpId: comment.dumpId,
|
||||
comment,
|
||||
@@ -637,13 +630,13 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
|
||||
case "comment_deleted": {
|
||||
const { commentId, dumpId } = msg;
|
||||
setLastCommentEvent({ type: "deleted", dumpId, commentId });
|
||||
events.emit("comment", { type: "deleted", dumpId, commentId });
|
||||
break;
|
||||
}
|
||||
|
||||
case "comment_updated": {
|
||||
const comment = deserializeComment(msg.comment);
|
||||
setLastCommentEvent({
|
||||
events.emit("comment", {
|
||||
type: "updated",
|
||||
dumpId: comment.dumpId,
|
||||
comment,
|
||||
@@ -653,12 +646,21 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
|
||||
case "notification_created": {
|
||||
const notification = deserializeNotification(msg.notification);
|
||||
setLastNotification(notification);
|
||||
events.emit("notification", notification);
|
||||
setUnreadNotificationCount((prev) => prev + 1);
|
||||
playChime("notify", notification.id);
|
||||
break;
|
||||
}
|
||||
|
||||
case "notifications_read":
|
||||
// Sent to every socket of the user, whichever tab read them.
|
||||
setUnreadNotificationCount(0);
|
||||
break;
|
||||
|
||||
case "categories_changed":
|
||||
events.emit("categories", null);
|
||||
break;
|
||||
|
||||
case "chat_message": {
|
||||
const message = deserializeChatMessage(msg.message);
|
||||
setChatMessages((prev) => {
|
||||
@@ -788,6 +790,7 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
}, [
|
||||
votes,
|
||||
likes,
|
||||
events,
|
||||
failPendingChat,
|
||||
isViewingChat,
|
||||
retirePendingChat,
|
||||
@@ -1125,14 +1128,8 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
myLikes,
|
||||
recentDumps,
|
||||
deletedDumpIds,
|
||||
lastVoteEvent,
|
||||
lastDumpEvent,
|
||||
lastPlaylistEvent,
|
||||
deletedPlaylistIds,
|
||||
lastCommentEvent,
|
||||
lastUserEvent,
|
||||
unreadNotificationCount,
|
||||
lastNotification,
|
||||
connectionEpoch,
|
||||
followsVersion,
|
||||
castVote,
|
||||
@@ -1150,14 +1147,8 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
myLikes,
|
||||
recentDumps,
|
||||
deletedDumpIds,
|
||||
lastVoteEvent,
|
||||
lastDumpEvent,
|
||||
lastPlaylistEvent,
|
||||
deletedPlaylistIds,
|
||||
lastCommentEvent,
|
||||
lastUserEvent,
|
||||
unreadNotificationCount,
|
||||
lastNotification,
|
||||
connectionEpoch,
|
||||
followsVersion,
|
||||
castVote,
|
||||
@@ -1213,13 +1204,15 @@ export function WSProvider({ children }: WSProviderProps): React.JSX.Element {
|
||||
|
||||
return (
|
||||
<WSContext.Provider value={value}>
|
||||
<WSChatContext.Provider value={chatValue}>
|
||||
<WSPresenceContext.Provider value={presenceValue}>
|
||||
<AvatarOverrideContext.Provider value={avatarOverrides}>
|
||||
{children}
|
||||
</AvatarOverrideContext.Provider>
|
||||
</WSPresenceContext.Provider>
|
||||
</WSChatContext.Provider>
|
||||
<WSEventsContext.Provider value={events}>
|
||||
<WSChatContext.Provider value={chatValue}>
|
||||
<WSPresenceContext.Provider value={presenceValue}>
|
||||
<AvatarOverrideContext.Provider value={avatarOverrides}>
|
||||
{children}
|
||||
</AvatarOverrideContext.Provider>
|
||||
</WSPresenceContext.Provider>
|
||||
</WSChatContext.Provider>
|
||||
</WSEventsContext.Provider>
|
||||
</WSContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user