From 3d9995622b3dcc0510055aece175172967a2d603 Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Thu, 28 Aug 2025 18:59:34 +0300
Subject: [PATCH] Refactor tournament realtime from SSE to Websocket (#2469)
---
app/components/Placeholder.module.css | 3 +
app/components/Placeholder.tsx | 6 +
.../build-analyzer/routes/analyzer.tsx | 3 +-
app/features/chat/chat-hooks.ts | 217 ++++++++++++++++-
app/features/chat/chat-types.ts | 29 ++-
app/features/chat/components/Chat.tsx | 220 +-----------------
app/features/map-planner/plans.css | 4 -
app/features/map-planner/routes/plans.tsx | 3 +-
.../sendouq-match/routes/q.match.$id.tsx | 4 +-
.../sendouq-settings/routes/q.settings.tsx | 4 +
app/features/sendouq/routes/q.looking.tsx | 3 +-
.../actions/to.$id.brackets.server.ts | 27 ++-
.../actions/to.$id.matches.$mid.server.ts | 55 ++---
.../components/StartedMatch.tsx | 3 +-
.../components/TournamentTeamActions.tsx | 31 ++-
.../core/emitters.server.ts | 11 -
.../routes/to.$id.brackets.subscribe.ts | 33 ---
.../routes/to.$id.brackets.tsx | 48 +---
.../routes/to.$id.matches.$mid.subscribe.ts | 30 ---
.../routes/to.$id.matches.$mid.tsx | 46 +---
.../tournament-bracket-utils.ts | 14 +-
app/features/tournament/routes/to.$id.tsx | 7 +-
app/features/tournament/tournament.css | 4 -
.../user-page/UserRepository.server.ts | 2 +-
app/routes.ts | 8 -
app/utils/urls.ts | 9 -
docs/dev/architecture.md | 27 ++-
locales/en/q.json | 1 +
.../static-assets/sounds/tournament_match.wav | Bin 0 -> 247340 bytes
29 files changed, 414 insertions(+), 438 deletions(-)
create mode 100644 app/components/Placeholder.module.css
create mode 100644 app/components/Placeholder.tsx
delete mode 100644 app/features/tournament-bracket/core/emitters.server.ts
delete mode 100644 app/features/tournament-bracket/routes/to.$id.brackets.subscribe.ts
delete mode 100644 app/features/tournament-bracket/routes/to.$id.matches.$mid.subscribe.ts
create mode 100644 public/static-assets/sounds/tournament_match.wav
diff --git a/app/components/Placeholder.module.css b/app/components/Placeholder.module.css
new file mode 100644
index 000000000..2ba624aab
--- /dev/null
+++ b/app/components/Placeholder.module.css
@@ -0,0 +1,3 @@
+.placeholder {
+ height: 100vh;
+}
diff --git a/app/components/Placeholder.tsx b/app/components/Placeholder.tsx
new file mode 100644
index 000000000..d41c8da14
--- /dev/null
+++ b/app/components/Placeholder.tsx
@@ -0,0 +1,6 @@
+import styles from "./Placeholder.module.css";
+
+/** Renders a blank placeholder component that can be used while content is loading. Better than returning null because it keeps the footer down where it belongs. */
+export function Placeholder() {
+ return
;
+}
diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx
index 940cd8110..2a9440922 100644
--- a/app/features/build-analyzer/routes/analyzer.tsx
+++ b/app/features/build-analyzer/routes/analyzer.tsx
@@ -86,6 +86,7 @@ import {
import "../analyzer.css";
import * as R from "remeda";
import { SendouSwitch } from "~/components/elements/Switch";
+import { Placeholder } from "~/components/Placeholder";
import { WeaponSelect } from "~/components/WeaponSelect";
import { logger } from "~/utils/logger";
@@ -117,7 +118,7 @@ export default function BuildAnalyzerShell() {
const isMounted = useIsMounted();
if (!isMounted) {
- return null;
+ return ;
}
return ;
diff --git a/app/features/chat/chat-hooks.ts b/app/features/chat/chat-hooks.ts
index 8552a1c15..1df882f35 100644
--- a/app/features/chat/chat-hooks.ts
+++ b/app/features/chat/chat-hooks.ts
@@ -1,6 +1,13 @@
+import { useRevalidator } from "@remix-run/react";
+import { nanoid } from "nanoid";
+import { WebSocket } from "partysocket";
import React from "react";
+import invariant from "~/utils/invariant";
+import { logger } from "~/utils/logger";
+import { soundPath } from "~/utils/urls";
import { useUser } from "../auth/core/user";
-import type { ChatMessage } from "./chat-types";
+import type { ChatMessage, ChatProps } from "./chat-types";
+import { messageTypeToSound, soundEnabled, soundVolume } from "./chat-utils";
// increasing this = scrolling happens even when scrolled more upwards
const THRESHOLD = 100;
@@ -69,3 +76,211 @@ export function useChatAutoScroll(
scrollToBottom,
};
}
+
+// TODO: should contain unseen messages logic, now it's duplicated
+export function useChat({
+ rooms,
+ onNewMessage,
+ revalidates = true,
+ connected = true,
+}: {
+ /** Which chat rooms to join. */
+ rooms: ChatProps["rooms"];
+ /** Callback function when a new chat message is received. Note: not fired for system messages. */
+ onNewMessage?: (message: ChatMessage) => void;
+ /** If false, skips revalidating on new message. Can be used if more fine grained control is needed regarding when the revalidation happens to e.g. preserve local state. Defaults to true. */
+ revalidates?: boolean;
+ /** If true, the chat is connected to the server. Defaults to true. */
+ connected?: boolean;
+}) {
+ const { revalidate } = useRevalidator();
+ const shouldRevalidate = React.useRef();
+ const user = useUser();
+
+ const [messages, setMessages] = React.useState([]);
+ const [readyState, setReadyState] = React.useState<
+ "CONNECTING" | "CONNECTED" | "CLOSED"
+ >("CONNECTING");
+ const [sentMessage, setSentMessage] = React.useState();
+ const [currentRoom, setCurrentRoom] = React.useState(
+ rooms[0]?.code,
+ );
+
+ const ws = React.useRef();
+ const lastSeenMessagesByRoomId = React.useRef