From a2a2de9bcb835047714e8ae1c4b8c419883ecf89 Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Thu, 12 Mar 2026 19:37:42 +0200
Subject: [PATCH] Chat sidebar work
---
app/components/layout/ChatSidebar.module.css | 31 ++++
app/components/layout/ChatSidebar.tsx | 23 ++-
app/components/layout/TopRightButtons.tsx | 51 ++++--
app/features/chat/ChatProvider.tsx | 146 ++++++++++++------
app/features/chat/ChatSystemMessage.server.ts | 19 ++-
app/features/chat/chat-constants.ts | 6 -
app/features/chat/routes/api.chat-users.ts | 1 -
.../scrims/loaders/scrims.$id.server.ts | 12 +-
.../loaders/q.match.$id.server.ts | 25 ++-
.../sendouq/loaders/q.looking.server.ts | 16 +-
.../loaders/to.$id.matches.$mid.server.ts | 34 ++--
app/utils/logger.ts | 1 +
12 files changed, 231 insertions(+), 134 deletions(-)
diff --git a/app/components/layout/ChatSidebar.module.css b/app/components/layout/ChatSidebar.module.css
index 654690937..a64593068 100644
--- a/app/components/layout/ChatSidebar.module.css
+++ b/app/components/layout/ChatSidebar.module.css
@@ -160,6 +160,29 @@
flex-shrink: 0;
}
+.chatHeaderInfo {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ min-width: 0;
+}
+
+.chatHeaderLink {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ min-width: 0;
+ text-decoration: none;
+ color: inherit;
+ padding: var(--s-1) var(--s-2);
+ border-radius: var(--radius-field);
+ transition: background-color 0.15s;
+}
+
+.chatHeaderLink:hover {
+ background-color: var(--color-bg-higher);
+}
+
.chatHeaderTitle {
font-size: var(--font-xs);
font-weight: var(--weight-semi);
@@ -168,6 +191,14 @@
white-space: nowrap;
}
+.chatHeaderSubtitle {
+ font-size: var(--font-2xs);
+ color: var(--color-text-high);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.emptyState {
padding: var(--s-4);
text-align: center;
diff --git a/app/components/layout/ChatSidebar.tsx b/app/components/layout/ChatSidebar.tsx
index 3becfd617..0ee482f32 100644
--- a/app/components/layout/ChatSidebar.tsx
+++ b/app/components/layout/ChatSidebar.tsx
@@ -1,12 +1,12 @@
import { ArrowLeft, MessageSquare, X } from "lucide-react";
import { Button } from "react-aria-components";
import { useTranslation } from "react-i18next";
+import { Link } from "react-router";
import { Chat } from "~/features/chat/components/Chat";
import { useChatContext } from "~/features/chat/useChatContext";
import { useTimeFormat } from "~/hooks/useTimeFormat";
import styles from "./ChatSidebar.module.css";
-// xxx: also make it so that if only one room then we go directly to the chat view instead of showing the room list
export function ChatSidebar({ onClose }: { onClose?: () => void }) {
const chatContext = useChatContext();
@@ -103,15 +103,30 @@ function ChatView({ onClose }: { onClose?: () => void }) {
chatContext.setActiveRoom(null);
};
+ const headerContent = (
+ <>
+
+ {room?.header ?? activeRoom}
+
+ {room?.subtitle ? (
+ {room.subtitle}
+ ) : null}
+ >
+ );
+
return (
-
- {room?.header ?? activeRoom}
-
+ {room?.url ? (
+
+ {headerContent}
+
+ ) : (
+
{headerContent}
+ )}
{onClose ? (
);
}
+
+function ChatButton({
+ variant,
+ onPress,
+ unreadCount,
+}: {
+ variant: "outlined" | "primary";
+ onPress: () => void;
+ unreadCount?: number;
+}) {
+ return (
+ <>
+
}
+ variant={variant}
+ onPress={onPress}
+ />
+ {unreadCount ? (
+
{unreadCount}
+ ) : null}
+ >
+ );
+}
diff --git a/app/features/chat/ChatProvider.tsx b/app/features/chat/ChatProvider.tsx
index 0879f48a1..130b2cc37 100644
--- a/app/features/chat/ChatProvider.tsx
+++ b/app/features/chat/ChatProvider.tsx
@@ -1,7 +1,7 @@
import { nanoid } from "nanoid";
import { WebSocket } from "partysocket";
import * as React from "react";
-import { useMatches, useRevalidator } from "react-router";
+import { useFetcher, useMatches, useRevalidator } from "react-router";
import { logger } from "~/utils/logger";
import { soundPath } from "~/utils/urls";
import type {
@@ -15,7 +15,6 @@ import { ChatContext } from "./useChatContext";
const PING_INTERVAL_MS = 60_000;
const LOCAL_STORAGE_PREFIX = "chat_read__";
-const USER_FETCH_BATCH_DELAY_MS = 200;
function flattenServerRoom(serverRoom: ServerRoomInfo): RoomInfo {
return {
@@ -91,9 +90,6 @@ function ChatProviderInner({
>({});
const ws = React.useRef
(undefined);
- const pendingUserFetches = React.useRef>(new Set());
- const fetchTimeoutRef =
- React.useRef>(undefined);
const computeUnreadCounts = React.useCallback((roomList: RoomInfo[]) => {
const counts: Record = {};
@@ -105,36 +101,13 @@ function ChatProviderInner({
setUnreadCounts(counts);
}, []);
- const fetchUnknownUsers = React.useCallback((userIds: number[]) => {
- for (const id of userIds) {
- pendingUserFetches.current.add(id);
- }
-
- clearTimeout(fetchTimeoutRef.current);
- fetchTimeoutRef.current = setTimeout(async () => {
- const ids = [...pendingUserFetches.current];
- pendingUserFetches.current.clear();
-
- if (ids.length === 0) return;
-
- try {
- // xxx: we should use remix fetcher for this
- const response = await fetch(`/api/chat-users?ids=${ids.join(",")}`);
- if (!response.ok) return;
-
- const users = (await response.json()) as Record;
- setChatUsersCache((prev) => ({ ...prev, ...users }));
- } catch {
- // fetch failed, will retry on next unknown user
- }
- }, USER_FETCH_BATCH_DELAY_MS);
- }, []);
-
const onMessage = React.useEffectEvent((e: MessageEvent) => {
const parsed = JSON.parse(e.data);
+ logger.debug("WS message received:", parsed);
// Initial rooms payload on connect
if (parsed.rooms && Array.isArray(parsed.rooms)) {
+ logger.debug("WS initial rooms payload, count:", parsed.rooms.length);
const serverRooms = parsed.rooms as ServerRoomInfo[];
const roomList = serverRooms.map(flattenServerRoom);
setRooms(roomList);
@@ -150,6 +123,7 @@ function ChatProviderInner({
// ROOM_JOINED: new room added
if (parsed.event === "ROOM_JOINED" && parsed.room) {
+ logger.debug("WS ROOM_JOINED:", parsed.room?.chatCode);
const serverRoom = parsed.room as ServerRoomInfo;
const newRoom = flattenServerRoom(serverRoom);
setRooms((prev) => {
@@ -167,6 +141,12 @@ function ChatProviderInner({
// CHAT_HISTORY response (also returned by SUBSCRIBE with metadata)
if (parsed.event === "CHAT_HISTORY" && Array.isArray(parsed.messages)) {
+ logger.debug(
+ "WS CHAT_HISTORY for:",
+ parsed.chatCode,
+ "messages:",
+ parsed.messages.length,
+ );
const chatCode = parsed.chatCode as string;
const messages = parsed.messages as ChatMessage[];
setMessagesByRoom((prev) => ({
@@ -206,6 +186,12 @@ function ChatProviderInner({
) as (ChatMessage & { totalMessageCount?: number })[];
const isSystemMessage = Boolean(messageArr[0].type);
+ logger.debug(
+ "WS message(s):",
+ messageArr.length,
+ "system:",
+ isSystemMessage,
+ );
if (isSystemMessage) {
revalidate();
}
@@ -238,10 +224,6 @@ function ChatProviderInner({
return { ...prev, [roomCode]: [...existing, msg] };
});
- if (msg.userId && !chatUsersCache[msg.userId]) {
- fetchUnknownUsers([msg.userId]);
- }
-
if (msg.totalMessageCount) {
setRooms((prev) =>
prev.map((r) =>
@@ -396,10 +378,21 @@ function ChatProviderInner({
if (open && activeRoom) {
markAsRead(activeRoom);
}
+
+ if (open && rooms.length === 1 && !activeRoom) {
+ requestHistory(rooms[0].chatCode);
+ setActiveRoom(rooms[0].chatCode);
+ }
},
- [activeRoom, markAsRead],
+ [activeRoom, markAsRead, requestHistory, rooms.length, rooms[0]?.chatCode],
);
+ useFetchUnknownChatUsers({
+ messages: messagesByRoom,
+ chatUsersCache,
+ setChatUsersCache,
+ });
+
useChatRouteSync({
rooms,
userId,
@@ -452,7 +445,6 @@ function ChatProviderInner({
);
}
-// xxx: bug: when viewing as non-participant can't go back to list view
// xxx: bug: room should close automatically if non-participant and leaves the route
// xxx: bug: route loading state should show before room metadata loads
function useChatRouteSync({
@@ -476,20 +468,11 @@ function useChatRouteSync({
React.SetStateAction>
>;
}) {
- const matches = useMatches();
+ const chatCode = useCurrentRouteChatCode();
const subscribedRoomRef = React.useRef(null);
+ const previousRouteChatCodeRef = React.useRef(null);
React.useEffect(() => {
- let chatCode: string | null = null;
-
- for (const match of matches) {
- const matchData = match.data as { chatCode?: string } | undefined;
- if (matchData?.chatCode) {
- chatCode = matchData.chatCode;
- break;
- }
- }
-
const previousSubscribed = subscribedRoomRef.current;
// Clean up previous non-participant subscription if chatCode changed
@@ -500,10 +483,15 @@ function useChatRouteSync({
const { [previousSubscribed]: _, ...rest } = prev;
return rest;
});
+ setActiveRoom(null);
+ setSidebarOpen(false);
subscribedRoomRef.current = null;
}
- if (!chatCode) return;
+ if (!chatCode) {
+ previousRouteChatCodeRef.current = null;
+ return;
+ }
const room = rooms.find((r) => r.chatCode === chatCode);
const isParticipant = room?.participantUserIds.includes(userId);
@@ -513,10 +501,15 @@ function useChatRouteSync({
subscribedRoomRef.current = chatCode;
}
- setActiveRoom(chatCode);
- setSidebarOpen(true);
+ const routeChatCodeChanged = previousRouteChatCodeRef.current !== chatCode;
+ previousRouteChatCodeRef.current = chatCode;
+
+ if (routeChatCodeChanged) {
+ setActiveRoom(chatCode);
+ setSidebarOpen(true);
+ }
}, [
- matches,
+ chatCode,
rooms,
userId,
setActiveRoom,
@@ -527,3 +520,54 @@ function useChatRouteSync({
setMessagesByRoom,
]);
}
+
+function useCurrentRouteChatCode() {
+ const matches = useMatches();
+
+ for (const match of matches) {
+ const matchData = match.data as { chatCode?: string } | undefined;
+ if (matchData?.chatCode) {
+ return matchData.chatCode;
+ }
+ }
+
+ return null;
+}
+
+function useFetchUnknownChatUsers({
+ messages,
+ chatUsersCache,
+ setChatUsersCache,
+}: {
+ messages: Record;
+ chatUsersCache: Record;
+ setChatUsersCache: React.Dispatch<
+ React.SetStateAction>
+ >;
+}) {
+ const fetcher = useFetcher>();
+
+ const unknownIds: number[] = [];
+ for (const msgs of Object.values(messages)) {
+ for (const msg of msgs) {
+ if (msg.userId && !chatUsersCache[msg.userId]) {
+ unknownIds.push(msg.userId);
+ }
+ }
+ }
+
+ const idsParam = unknownIds.sort((a, b) => a - b).join(",");
+
+ React.useEffect(() => {
+ if (!idsParam || fetcher.state !== "idle") return;
+
+ logger.debug(`Fetching unknown chat users: ${idsParam}`);
+ fetcher.load(`/api/chat-users?ids=${idsParam}`);
+ }, [idsParam, fetcher.load, fetcher.state]);
+
+ React.useEffect(() => {
+ if (!fetcher.data) return;
+
+ setChatUsersCache((prev) => ({ ...prev, ...fetcher.data }));
+ }, [fetcher.data, setChatUsersCache]);
+}
diff --git a/app/features/chat/ChatSystemMessage.server.ts b/app/features/chat/ChatSystemMessage.server.ts
index 9eaf0bf70..e8c30c8a0 100644
--- a/app/features/chat/ChatSystemMessage.server.ts
+++ b/app/features/chat/ChatSystemMessage.server.ts
@@ -1,8 +1,10 @@
+import { add } from "date-fns";
import { nanoid } from "nanoid";
+import * as UserRepository from "~/features/user-page/UserRepository.server";
import { IS_E2E_TEST_RUN } from "~/utils/e2e";
import invariant from "~/utils/invariant";
import { logger } from "~/utils/logger";
-import type { ChatMessage, ChatUser } from "./chat-types";
+import type { ChatMessage } from "./chat-types";
const SKALOP_TOKEN_HEADER_NAME = "Skalop-Token";
@@ -64,8 +66,7 @@ interface SetMetadataArgs {
subtitle: string;
url: string;
participantUserIds: number[];
- chatUsers: Record;
- expiresAt: number;
+ expiresAfter: { hours: number } | { days: number };
}
// xxx: actually there is no dedup like this, for as long as the service is up, we dont resend metadata
@@ -73,7 +74,7 @@ const DEDUP_INTERVAL_MS = 30_000;
const DEDUP_PRUNE_MS = 1000 * 60 * 60 * 24 * 30;
const metadataDedup = new Map();
-export function setMetadata(args: SetMetadataArgs) {
+export async function setMetadata(args: SetMetadataArgs) {
if (systemMessagesDisabled) return;
if (!process.env.SKALOP_SYSTEM_MESSAGE_URL) return;
@@ -89,6 +90,12 @@ export function setMetadata(args: SetMetadataArgs) {
}
}
+ const expiresAt = add(new Date(), args.expiresAfter).getTime();
+
+ const chatUsers = await UserRepository.findChatUsersByUserIds(
+ args.participantUserIds,
+ );
+
return void fetch(process.env.SKALOP_SYSTEM_MESSAGE_URL, {
method: "POST",
body: JSON.stringify({
@@ -96,8 +103,8 @@ export function setMetadata(args: SetMetadataArgs) {
chatCode: args.chatCode,
metadata: {
participantUserIds: args.participantUserIds,
- chatUsers: args.chatUsers,
- expiresAt: args.expiresAt,
+ chatUsers,
+ expiresAt,
header: args.header,
subtitle: args.subtitle,
url: args.url,
diff --git a/app/features/chat/chat-constants.ts b/app/features/chat/chat-constants.ts
index cb2fc7b60..8e0f93316 100644
--- a/app/features/chat/chat-constants.ts
+++ b/app/features/chat/chat-constants.ts
@@ -1,7 +1 @@
export const MESSAGE_MAX_LENGTH = 200;
-
-// xxx: use date-fns
-export const SENDOUQ_MATCH_EXPIRY_MS = 1000 * 60 * 60;
-export const TOURNAMENT_MATCH_EXPIRY_MS = 1000 * 60 * 60 * 2;
-export const LEAGUE_MATCH_EXPIRY_MS = 1000 * 60 * 60 * 24 * 30;
-export const SCRIM_EXPIRY_MS = 1000 * 60 * 60 * 3;
diff --git a/app/features/chat/routes/api.chat-users.ts b/app/features/chat/routes/api.chat-users.ts
index 0b59dfb22..68eb06829 100644
--- a/app/features/chat/routes/api.chat-users.ts
+++ b/app/features/chat/routes/api.chat-users.ts
@@ -12,7 +12,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
return Response.json({});
}
- // xxx: use zod
const parsedIds = idsParam
.split(",")
.map(Number)
diff --git a/app/features/scrims/loaders/scrims.$id.server.ts b/app/features/scrims/loaders/scrims.$id.server.ts
index e19be6555..0e2817edf 100644
--- a/app/features/scrims/loaders/scrims.$id.server.ts
+++ b/app/features/scrims/loaders/scrims.$id.server.ts
@@ -1,6 +1,5 @@
import type { LoaderFunctionArgs } from "react-router";
-import { setMetadata } from "~/features/chat/ChatSystemMessage.server";
-import { SCRIM_EXPIRY_MS } from "~/features/chat/chat-constants";
+import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
import { tournamentDataCached } from "~/features/tournament-bracket/core/Tournament.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { notFoundIfFalsy } from "../../../utils/remix.server";
@@ -27,26 +26,23 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
}
const participantIds = Scrim.participantIdsListFromAccepted(post);
- const chatUsers = await UserRepository.findChatUsersByUserIds(participantIds);
// xxx: additional condition
if (post.chatCode) {
- setMetadata({
+ ChatSystemMessage.setMetadata({
chatCode: post.chatCode,
// xxx: better header+subtitle
header: "Scrim",
subtitle: `Scrim #${post.id}`,
url: `/scrims/${post.id}`,
participantUserIds: participantIds,
- chatUsers,
- expiresAt: Date.now() + SCRIM_EXPIRY_MS,
+ expiresAfter: { hours: 3 },
});
}
return {
post,
- // xxx: only chatCode if permissions
- chatCode: post.chatCode,
+ chatCode: user.roles.includes("STAFF") ? post.chatCode : undefined,
anyUserPrefersNoScreen:
await UserRepository.anyUserPrefersNoScreen(participantIds),
tournamentMapPool: post.mapsTournament
diff --git a/app/features/sendouq-match/loaders/q.match.$id.server.ts b/app/features/sendouq-match/loaders/q.match.$id.server.ts
index f268e8ff3..7ecd57750 100644
--- a/app/features/sendouq-match/loaders/q.match.$id.server.ts
+++ b/app/features/sendouq-match/loaders/q.match.$id.server.ts
@@ -1,14 +1,13 @@
import type { LoaderFunctionArgs } from "react-router";
import { getUser } from "~/features/auth/core/user.server";
-import { setMetadata } from "~/features/chat/ChatSystemMessage.server";
-import { SENDOUQ_MATCH_EXPIRY_MS } from "~/features/chat/chat-constants";
+import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
import { SendouQ } from "~/features/sendouq/core/SendouQ.server";
import * as PrivateUserNoteRepository from "~/features/sendouq/PrivateUserNoteRepository.server";
import { reportedWeaponsToArrayOfArrays } from "~/features/sendouq-match/core/reported-weapons.server";
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
-import * as UserRepository from "~/features/user-page/UserRepository.server";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
+import { sendouQMatchPage } from "~/utils/urls";
import { qMatchPageParamsSchema } from "../q-match-schemas";
export const loader = async ({ params }: LoaderFunctionArgs) => {
@@ -35,24 +34,19 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
? await ReportedWeaponRepository.findByMatchId(matchId)
: null;
- // xxx: also if match is even ongoing
- if (match.chatCode && user) {
+ if (match.chatCode && !match.isLocked) {
const participantIds = [
...matchUnmapped.groupAlpha.members,
...matchUnmapped.groupBravo.members,
].map((m) => m.id);
- const chatUsers =
- await UserRepository.findChatUsersByUserIds(participantIds);
-
- setMetadata({
+ ChatSystemMessage.setMetadata({
chatCode: match.chatCode,
- header: "SQ Match",
- subtitle: `Match #${matchId}`,
- url: `/q/match/${matchId}`,
+ header: `Match #${matchId}`,
+ subtitle: "SendouQ",
+ url: sendouQMatchPage(matchId),
participantUserIds: participantIds,
- chatUsers,
- expiresAt: Date.now() + SENDOUQ_MATCH_EXPIRY_MS,
+ expiresAfter: { hours: 2 },
});
}
@@ -67,7 +61,6 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
})
: null,
rawReportedWeapons,
- // xxx: only chatCode if permissions
- chatCode: match.chatCode,
+ chatCode: user?.roles.includes("STAFF") ? match.chatCode : null,
};
};
diff --git a/app/features/sendouq/loaders/q.looking.server.ts b/app/features/sendouq/loaders/q.looking.server.ts
index 9389b54f1..1d43e5705 100644
--- a/app/features/sendouq/loaders/q.looking.server.ts
+++ b/app/features/sendouq/loaders/q.looking.server.ts
@@ -1,10 +1,8 @@
import type { LoaderFunctionArgs } from "react-router";
import { requireUser } from "~/features/auth/core/user.server";
-import { setMetadata } from "~/features/chat/ChatSystemMessage.server";
-import { SENDOUQ_MATCH_EXPIRY_MS } from "~/features/chat/chat-constants";
+import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
import * as SQGroupRepository from "~/features/sendouq/SQGroupRepository.server";
import { cachedStreams } from "~/features/sendouq-streams/core/streams.server";
-import * as UserRepository from "~/features/user-page/UserRepository.server";
import { groupExpiryStatus } from "../core/groups";
import { SendouQ } from "../core/SendouQ.server";
import * as PrivateUserNoteRepository from "../PrivateUserNoteRepository.server";
@@ -35,19 +33,17 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
});
}
+ // xxx: how to handle group changing..?
if (ownGroup?.chatCode) {
const memberIds = ownGroup.members.map((m: { id: number }) => m.id);
- const chatUsers = await UserRepository.findChatUsersByUserIds(memberIds);
- setMetadata({
+ ChatSystemMessage.setMetadata({
chatCode: ownGroup.chatCode,
- header: "SQ Group",
- // xxx: better subtitle
- subtitle: "Looking for match",
+ header: "Group",
+ subtitle: "SendouQ",
url: "/q/looking",
participantUserIds: memberIds,
- chatUsers,
- expiresAt: Date.now() + SENDOUQ_MATCH_EXPIRY_MS,
+ expiresAfter: { hours: 1 },
});
}
diff --git a/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts b/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts
index 4c5b9c297..7a3a96591 100644
--- a/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts
+++ b/app/features/tournament-bracket/loaders/to.$id.matches.$mid.server.ts
@@ -1,7 +1,7 @@
import cachified from "@epic-web/cachified";
import type { LoaderFunctionArgs } from "react-router";
-import { setMetadata } from "~/features/chat/ChatSystemMessage.server";
-import { TOURNAMENT_MATCH_EXPIRY_MS } from "~/features/chat/chat-constants";
+import { getUser } from "~/features/auth/core/user.server";
+import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
@@ -9,7 +9,9 @@ import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
import { IS_E2E_TEST_RUN } from "~/utils/e2e";
import { logger } from "~/utils/logger";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
+import { tournamentMatchPage } from "~/utils/urls";
import { mapListFromResults, resolveMapList } from "../core/mapList.server";
+import { tournamentFromDBCached } from "../core/Tournament.server";
import { findMatchById } from "../queries/findMatchById.server";
import { findResultsByMatchId } from "../queries/findResultsByMatchId.server";
import { matchPageParamsSchema } from "../tournament-bracket-schemas.server";
@@ -22,6 +24,11 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
params,
schema: matchPageParamsSchema,
});
+ const user = getUser();
+ const tournament = await tournamentFromDBCached({
+ tournamentId,
+ user: undefined,
+ });
const match = notFoundIfFalsy(findMatchById(matchId));
@@ -94,21 +101,17 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
})
: false;
- // xxx: also need to check match is currently ongoing
- if (match.chatCode) {
+ if (match.chatCode && !matchIsOver) {
const playerIds = match.players.map((p) => p.id);
- // xxx: should this be resolved inside setMetadata?
- const chatUsers = await UserRepository.findChatUsersByUserIds(playerIds);
+ const matchContext = tournament.matchContextNamesById(matchId);
- // xxx: module.function format
- setMetadata({
+ ChatSystemMessage.setMetadata({
chatCode: match.chatCode,
- header: "Tournament Match",
- subtitle: `Match #${matchId}`,
- url: `/to/${tournamentId}/matches/${matchId}`,
+ header: matchContext.roundName ?? `Match #${matchId}`,
+ subtitle: tournament.ctx.name,
+ url: tournamentMatchPage({ tournamentId, matchId }),
participantUserIds: playerIds,
- chatUsers,
- expiresAt: Date.now() + TOURNAMENT_MATCH_EXPIRY_MS,
+ expiresAfter: { hours: 2 },
});
}
@@ -119,7 +122,8 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
matchIsOver,
endedEarly,
noScreen,
- // xxx: only chatCode if permissions
- chatCode: match.chatCode,
+ chatCode: tournament.isOrganizerOrStreamer(user)
+ ? match.chatCode
+ : undefined,
};
};
diff --git a/app/utils/logger.ts b/app/utils/logger.ts
index 631261fdb..b3ac0d27f 100644
--- a/app/utils/logger.ts
+++ b/app/utils/logger.ts
@@ -22,4 +22,5 @@ export const logger = {
info: (...args: unknown[]) => console.log(...formatLog(...args)),
error: (...args: unknown[]) => console.error(...formatLog(...args)),
warn: (...args: unknown[]) => console.warn(...formatLog(...args)),
+ debug: (...args: unknown[]) => console.debug(...formatLog(...args)),
};