mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-23 03:46:28 -05:00
Fix WS revalidate on tournament bracket & match pages (#3061)
This commit is contained in:
parent
eafd10f3a9
commit
20118fb7a5
|
|
@ -277,7 +277,11 @@ function ChatProviderInner({
|
|||
isSystemMessage,
|
||||
);
|
||||
if (isSystemMessage || messageArr[0].revalidateOnly) {
|
||||
revalidate();
|
||||
// The actor that triggered this revalidate is the current user — their
|
||||
// own form submission already reran loaders, so skip the duplicate fetch.
|
||||
const isOwnRevalidate =
|
||||
messageArr[0].revalidateOnly && messageArr[0].authorUserId === userId;
|
||||
if (!isOwnRevalidate) revalidate();
|
||||
}
|
||||
|
||||
const sound = messageTypeToSound(messageArr[0].type);
|
||||
|
|
@ -444,6 +448,14 @@ function ChatProviderInner({
|
|||
ws.current?.send(JSON.stringify({ event: "UNSUBSCRIBE", chatCode }));
|
||||
}, []);
|
||||
|
||||
const subscribeTopic = React.useCallback((topic: string) => {
|
||||
ws.current?.send(JSON.stringify({ event: "SUBSCRIBE_TOPIC", topic }));
|
||||
}, []);
|
||||
|
||||
const unsubscribeTopic = React.useCallback((topic: string) => {
|
||||
ws.current?.send(JSON.stringify({ event: "UNSUBSCRIBE_TOPIC", topic }));
|
||||
}, []);
|
||||
|
||||
const requestHistory = React.useCallback((chatCode: string) => {
|
||||
ws.current?.send(JSON.stringify({ event: "CHAT_HISTORY", chatCode }));
|
||||
}, []);
|
||||
|
|
@ -508,6 +520,8 @@ function ChatProviderInner({
|
|||
send,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
subscribeTopic,
|
||||
unsubscribeTopic,
|
||||
requestHistory,
|
||||
markAsRead,
|
||||
unreadCounts,
|
||||
|
|
@ -529,6 +543,8 @@ function ChatProviderInner({
|
|||
send,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
subscribeTopic,
|
||||
unsubscribeTopic,
|
||||
requestHistory,
|
||||
markAsRead,
|
||||
unreadCounts,
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ function logSkalpError(action: string) {
|
|||
|
||||
type PartialChatMessage = Pick<
|
||||
ChatMessage,
|
||||
"type" | "context" | "room" | "revalidateOnly"
|
||||
"type" | "context" | "room" | "revalidateOnly" | "authorUserId"
|
||||
>;
|
||||
interface ChatSystemMessageService {
|
||||
send: (msg: PartialChatMessage | PartialChatMessage[]) => undefined;
|
||||
|
|
@ -59,6 +59,7 @@ export const send: ChatSystemMessageService["send"] = (partialMsg) => {
|
|||
context: partialMsg.context,
|
||||
type: partialMsg.type,
|
||||
revalidateOnly: partialMsg.revalidateOnly,
|
||||
authorUserId: partialMsg.authorUserId,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as React from "react";
|
||||
import { useUser } from "../auth/core/user";
|
||||
import type { ChatMessage } from "./chat-types";
|
||||
import { useChatContext } from "./useChatContext";
|
||||
|
||||
// increasing this = scrolling happens even when scrolled more upwards
|
||||
const THRESHOLD = 100;
|
||||
|
|
@ -69,3 +70,25 @@ export function useChatAutoScroll(
|
|||
scrollToBottom,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribes the page to a Skalop topic over the shared chat WebSocket so that
|
||||
* `revalidateOnly` broadcasts to the topic trigger a data loader revalidation.
|
||||
* Topics are lightweight: no metadata, no participants, no history — purely a
|
||||
* fan-out channel. Pass `connected=false` to opt out (e.g. once a tournament
|
||||
* has been finalized and no further updates are expected).
|
||||
*/
|
||||
export function useWebsocketRevalidation(topic: string, connected = true) {
|
||||
const chat = useChatContext();
|
||||
const subscribeTopic = chat?.subscribeTopic;
|
||||
const unsubscribeTopic = chat?.unsubscribeTopic;
|
||||
const readyState = chat?.readyState;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!connected || readyState !== "CONNECTED") return;
|
||||
if (!subscribeTopic || !unsubscribeTopic) return;
|
||||
|
||||
subscribeTopic(topic);
|
||||
return () => unsubscribeTopic(topic);
|
||||
}, [topic, connected, readyState, subscribeTopic, unsubscribeTopic]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export interface ChatContextValue {
|
|||
send: (chatCode: string, contents: string) => void;
|
||||
subscribe: (chatCode: string) => void;
|
||||
unsubscribe: (chatCode: string) => void;
|
||||
subscribeTopic: (topic: string) => void;
|
||||
unsubscribeTopic: (topic: string) => void;
|
||||
requestHistory: (chatCode: string) => void;
|
||||
markAsRead: (chatCode: string) => void;
|
||||
unreadCounts: Record<string, number>;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ export interface ChatMessage {
|
|||
context?: SystemMessageContext;
|
||||
/** If true, the purpose of this message is just to run the data loaders again meaning the logic related to showing a new chat message is skipped. Defaults to false. */
|
||||
revalidateOnly?: boolean;
|
||||
/** User id of the actor that triggered this message. Used to skip own-author revalidates so we don't double-fetch loaders right after a form submission. */
|
||||
authorUserId?: number;
|
||||
userId?: number;
|
||||
timestamp: number;
|
||||
room: string;
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -154,6 +155,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -226,6 +228,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -280,6 +283,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -304,6 +308,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -384,6 +389,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
|
|||
ChatSystemMessage.send({
|
||||
room: match.chatCode,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
room: targetChatCode,
|
||||
type: "LIKE_RECEIVED",
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +79,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
room: targetChatCode,
|
||||
type: "LIKE_RECEIVED",
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
|
@ -201,11 +203,13 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
room: ownGroup.chatCode,
|
||||
type: "MATCH_STARTED",
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
},
|
||||
{
|
||||
room: theirGroup.chatCode,
|
||||
type: "MATCH_STARTED",
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
ChatSystemMessage.send({
|
||||
room: chatCodeToRevalidate,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
ChatSystemMessage.send({
|
||||
room: chatCodeToRevalidate,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +79,7 @@ export const action: ActionFunction = async ({ request }) => {
|
|||
ChatSystemMessage.send({
|
||||
room: chatCodeToRevalidate,
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
room: tournamentWebsocketRoom(tournament.ctx.id),
|
||||
type: "TOURNAMENT_UPDATED",
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import {
|
|||
SendouTabs,
|
||||
} from "~/components/elements/Tabs";
|
||||
import { useUser } from "~/features/auth/core/user";
|
||||
import { useWebsocketRevalidation } from "~/features/chat/chat-hooks";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import { useHydrated } from "~/hooks/useHydrated";
|
||||
import { useSearchParamState } from "~/hooks/useSearchParamState";
|
||||
|
|
@ -44,6 +45,7 @@ import * as AbDivisions from "../core/AbDivisions";
|
|||
import type { Bracket as BracketType } from "../core/Bracket";
|
||||
import * as PreparedMaps from "../core/PreparedMaps";
|
||||
import type { Tournament } from "../core/Tournament";
|
||||
import { tournamentWebsocketRoom } from "../tournament-bracket-utils";
|
||||
|
||||
export { action };
|
||||
|
||||
|
|
@ -81,6 +83,11 @@ export default function TournamentBracketsPage() {
|
|||
[tournament, bracketIdx],
|
||||
);
|
||||
|
||||
useWebsocketRevalidation(
|
||||
tournamentWebsocketRoom(tournament.ctx.id),
|
||||
!tournament.ctx.isFinalized,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (visibility !== "visible" || tournament.ctx.isFinalized) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -813,18 +813,19 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
|
||||
clearTournamentDataCache(tournamentId);
|
||||
|
||||
// TODO: we could optimize this in the future by including an `authorUserId` field and skip revalidation if the author is the same as the current user
|
||||
if (emitMatchUpdate) {
|
||||
ChatSystemMessage.send([
|
||||
{
|
||||
room: tournamentMatchWebsocketRoom(matchId),
|
||||
type: "TOURNAMENT_MATCH_UPDATED",
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
},
|
||||
...endedDroppedMatchIds.map((id) => ({
|
||||
room: tournamentMatchWebsocketRoom(id),
|
||||
type: "TOURNAMENT_MATCH_UPDATED" as const,
|
||||
revalidateOnly: true as const,
|
||||
authorUserId: user.id,
|
||||
})),
|
||||
]);
|
||||
}
|
||||
|
|
@ -834,6 +835,7 @@ export const action: ActionFunction = async ({ params, request }) => {
|
|||
room: tournamentWebsocketRoom(tournament.ctx.id),
|
||||
type: "TOURNAMENT_UPDATED",
|
||||
revalidateOnly: true,
|
||||
authorUserId: user.id,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useLoaderData } from "react-router";
|
||||
import { containerClassName } from "~/components/Main";
|
||||
import { MatchPage } from "~/components/match-page/MatchPage";
|
||||
import { useWebsocketRevalidation } from "~/features/chat/chat-hooks";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { action } from "../actions/to.$id.matches.$mid.server";
|
||||
import { TournamentMatchBanner } from "../components/TournamentMatchBanner";
|
||||
|
|
@ -8,6 +9,7 @@ import { TournamentMatchHeader } from "../components/TournamentMatchHeader";
|
|||
import { TournamentMatchTabs } from "../components/TournamentMatchTabs";
|
||||
import { loader } from "../loaders/to.$id.matches.$mid.server";
|
||||
import { MatchPageProvider } from "../match-page-context";
|
||||
import { tournamentMatchWebsocketRoom } from "../tournament-match-utils";
|
||||
|
||||
export { action, loader };
|
||||
|
||||
|
|
@ -18,6 +20,11 @@ export const handle: SendouRouteHandle = {
|
|||
export default function TournamentMatchPage() {
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
useWebsocketRevalidation(
|
||||
tournamentMatchWebsocketRoom(data.match.id),
|
||||
!data.matchIsOver,
|
||||
);
|
||||
|
||||
return (
|
||||
<MatchPageProvider data={data}>
|
||||
<MatchPage className={containerClassName("normal")}>
|
||||
|
|
|
|||
|
|
@ -410,11 +410,13 @@ export const action: ActionFunction = async ({ request, params }) => {
|
|||
room: tournamentMatchWebsocketRoom(matchId),
|
||||
type: "TOURNAMENT_MATCH_UPDATED" as const,
|
||||
revalidateOnly: true as const,
|
||||
authorUserId: user.id,
|
||||
})),
|
||||
{
|
||||
room: tournamentWebsocketRoom(tournament.ctx.id),
|
||||
type: "TOURNAMENT_UPDATED" as const,
|
||||
revalidateOnly: true as const,
|
||||
authorUserId: user.id,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user