mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-05 20:56:13 -05:00
Chat sidebar work
This commit is contained in:
parent
d884cb3f3b
commit
a2a2de9bcb
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
<>
|
||||
<span className={styles.chatHeaderTitle}>
|
||||
{room?.header ?? activeRoom}
|
||||
</span>
|
||||
{room?.subtitle ? (
|
||||
<span className={styles.chatHeaderSubtitle}>{room.subtitle}</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.sidebar}>
|
||||
<div className={styles.chatHeader}>
|
||||
<Button className={styles.backButton} onPress={handleBack}>
|
||||
<ArrowLeft size={18} />
|
||||
</Button>
|
||||
<span className={styles.chatHeaderTitle}>
|
||||
{room?.header ?? activeRoom}
|
||||
</span>
|
||||
{room?.url ? (
|
||||
<Link to={room.url} className={styles.chatHeaderLink}>
|
||||
{headerContent}
|
||||
</Link>
|
||||
) : (
|
||||
<div className={styles.chatHeaderInfo}>{headerContent}</div>
|
||||
)}
|
||||
{onClose ? (
|
||||
<Button className={styles.closeButton} onPress={onClose}>
|
||||
<X size={18} />
|
||||
|
|
|
|||
|
|
@ -24,21 +24,6 @@ export function TopRightButtons({
|
|||
}) {
|
||||
const { t } = useTranslation(["common", "front"]);
|
||||
|
||||
// xxx: anti-pattern? probablty just extract this
|
||||
const chatButton = (variant: "outlined" | "primary", onPress: () => void) => (
|
||||
<>
|
||||
<SendouButton
|
||||
size="small"
|
||||
icon={<MessageSquare />}
|
||||
variant={variant}
|
||||
onPress={onPress}
|
||||
/>
|
||||
{chatUnreadCount ? (
|
||||
<span className={styles.chatUnreadBadge}>{chatUnreadCount}</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{showSupport ? (
|
||||
|
|
@ -65,12 +50,20 @@ export function TopRightButtons({
|
|||
</div>
|
||||
{onChatToggle ? (
|
||||
<div className={styles.chatButtonWrapperPersistent}>
|
||||
{chatButton("outlined", onChatToggle)}
|
||||
<ChatButton
|
||||
variant="outlined"
|
||||
onPress={onChatToggle}
|
||||
unreadCount={chatUnreadCount}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{onChatModalToggle ? (
|
||||
<div className={styles.chatButtonWrapperModal}>
|
||||
{chatButton("outlined", onChatModalToggle)}
|
||||
<ChatButton
|
||||
variant="outlined"
|
||||
onPress={onChatModalToggle}
|
||||
unreadCount={chatUnreadCount}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
|
|
@ -84,3 +77,27 @@ export function TopRightButtons({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatButton({
|
||||
variant,
|
||||
onPress,
|
||||
unreadCount,
|
||||
}: {
|
||||
variant: "outlined" | "primary";
|
||||
onPress: () => void;
|
||||
unreadCount?: number;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<SendouButton
|
||||
size="small"
|
||||
icon={<MessageSquare />}
|
||||
variant={variant}
|
||||
onPress={onPress}
|
||||
/>
|
||||
{unreadCount ? (
|
||||
<span className={styles.chatUnreadBadge}>{unreadCount}</span>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<WebSocket>(undefined);
|
||||
const pendingUserFetches = React.useRef<Set<number>>(new Set());
|
||||
const fetchTimeoutRef =
|
||||
React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
const computeUnreadCounts = React.useCallback((roomList: RoomInfo[]) => {
|
||||
const counts: Record<string, number> = {};
|
||||
|
|
@ -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<number, ChatUser>;
|
||||
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<Record<string, ChatMessage[]>>
|
||||
>;
|
||||
}) {
|
||||
const matches = useMatches();
|
||||
const chatCode = useCurrentRouteChatCode();
|
||||
const subscribedRoomRef = React.useRef<string | null>(null);
|
||||
const previousRouteChatCodeRef = React.useRef<string | null>(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<string, ChatMessage[]>;
|
||||
chatUsersCache: Record<number, ChatUser>;
|
||||
setChatUsersCache: React.Dispatch<
|
||||
React.SetStateAction<Record<number, ChatUser>>
|
||||
>;
|
||||
}) {
|
||||
const fetcher = useFetcher<Record<number, ChatUser>>();
|
||||
|
||||
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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<number, ChatUser>;
|
||||
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<string, number>();
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
|||
return Response.json({});
|
||||
}
|
||||
|
||||
// xxx: use zod
|
||||
const parsedIds = idsParam
|
||||
.split(",")
|
||||
.map(Number)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user