From 1edb2809b50fcdb3458f1fc7d11b37304699d60e Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:56:53 +0200 Subject: [PATCH] Expiring chat codes --- app/features/chat/chat-utils.test.ts | 49 ++++++++++++++++++- app/features/chat/chat-utils.ts | 15 ++++++ app/features/chat/useChatContext.ts | 2 - .../scrims/loaders/scrims.$id.server.ts | 9 +++- .../loaders/q.match.$id.server.ts | 10 +++- .../loaders/to.$id.matches.$mid.server.ts | 15 +++++- 6 files changed, 94 insertions(+), 6 deletions(-) diff --git a/app/features/chat/chat-utils.test.ts b/app/features/chat/chat-utils.test.ts index f6c8349c5..513fb8889 100644 --- a/app/features/chat/chat-utils.test.ts +++ b/app/features/chat/chat-utils.test.ts @@ -1,5 +1,52 @@ +import { sub } from "date-fns"; import { describe, expect, test } from "vitest"; -import { datePlaceholder, resolveDatePlaceholders } from "./chat-utils"; +import { + chatAccessible, + datePlaceholder, + resolveDatePlaceholders, +} from "./chat-utils"; + +describe("chatCodeVisible", () => { + test("visible when within expiration window", () => { + const result = chatAccessible({ + isStaff: false, + expiresAfterDays: 1, + comparedTo: new Date(), + }); + + expect(result).toBe(true); + }); + + test("not visible when past expiration window", () => { + const result = chatAccessible({ + isStaff: false, + expiresAfterDays: 1, + comparedTo: sub(new Date(), { days: 3 }), + }); + + expect(result).toBe(false); + }); + + test("staff gets 7 extra days", () => { + const result = chatAccessible({ + isStaff: true, + expiresAfterDays: 1, + comparedTo: sub(new Date(), { days: 5 }), + }); + + expect(result).toBe(true); + }); + + test("staff extra days are not infinite", () => { + const result = chatAccessible({ + isStaff: true, + expiresAfterDays: 1, + comparedTo: sub(new Date(), { days: 10 }), + }); + + expect(result).toBe(false); + }); +}); describe("datePlaceholder", () => { test("returns correctly formatted placeholder string", () => { diff --git a/app/features/chat/chat-utils.ts b/app/features/chat/chat-utils.ts index 56de7d0e1..9cc422422 100644 --- a/app/features/chat/chat-utils.ts +++ b/app/features/chat/chat-utils.ts @@ -1,5 +1,20 @@ +import { differenceInDays } from "date-fns"; import type { ChatMessage } from "./chat-types"; +const STAFF_EXTRA_DAYS = 7; + +export function chatAccessible(args: { + isStaff: boolean; + expiresAfterDays: number; + comparedTo: Date; +}): boolean { + const extraDays = args.isStaff ? STAFF_EXTRA_DAYS : 0; + return ( + differenceInDays(new Date(), args.comparedTo) <= + args.expiresAfterDays + extraDays + ); +} + const DATE_PLACEHOLDER_PATTERN = /\{\{date:(\d+)\}\}/g; export function datePlaceholder(date: Date): string { diff --git a/app/features/chat/useChatContext.ts b/app/features/chat/useChatContext.ts index 968fcdecb..7e83cac53 100644 --- a/app/features/chat/useChatContext.ts +++ b/app/features/chat/useChatContext.ts @@ -1,8 +1,6 @@ import * as React from "react"; import type { ChatContextValue } from "./chat-provider-types"; -// xxx: think how chats should expire in relation to chatCode getting returned from loaders etc. - export const ChatContext = React.createContext(null); export function useChatContext(): ChatContextValue | null { diff --git a/app/features/scrims/loaders/scrims.$id.server.ts b/app/features/scrims/loaders/scrims.$id.server.ts index f37b44da6..e7486b1fe 100644 --- a/app/features/scrims/loaders/scrims.$id.server.ts +++ b/app/features/scrims/loaders/scrims.$id.server.ts @@ -1,6 +1,8 @@ import type { LoaderFunctionArgs } from "react-router"; +import { chatAccessible } from "~/features/chat/chat-utils"; import { tournamentDataCached } from "~/features/tournament-bracket/core/Tournament.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; +import { databaseTimestampToDate } from "~/utils/dates"; import { notFoundIfFalsy } from "../../../utils/remix.server"; import { type AuthenticatedUser, @@ -29,7 +31,12 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { return { post, chatCode: - user.roles.includes("STAFF") || participantIds.includes(user.id) + (user.roles.includes("STAFF") || participantIds.includes(user.id)) && + chatAccessible({ + isStaff: user.roles.includes("STAFF"), + expiresAfterDays: 1, + comparedTo: databaseTimestampToDate(Scrim.getStartTime(post)), + }) ? post.chatCode : undefined, anyUserPrefersNoScreen: 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 97c1379ff..46f6347db 100644 --- a/app/features/sendouq-match/loaders/q.match.$id.server.ts +++ b/app/features/sendouq-match/loaders/q.match.$id.server.ts @@ -1,10 +1,12 @@ import type { LoaderFunctionArgs } from "react-router"; import { getUser } from "~/features/auth/core/user.server"; +import { chatAccessible } from "~/features/chat/chat-utils"; 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 { databaseTimestampToDate } from "~/utils/dates"; import { notFoundIfFalsy, parseParams } from "~/utils/remix.server"; import { qMatchPageParamsSchema } from "../q-match-schemas"; @@ -44,7 +46,13 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { : null, rawReportedWeapons, chatCode: - user?.roles.includes("STAFF") || (user && matchUsers.includes(user.id)) + (user?.roles.includes("STAFF") || + (user && matchUsers.includes(user.id))) && + chatAccessible({ + isStaff: user?.roles.includes("STAFF") ?? false, + expiresAfterDays: 1, + comparedTo: databaseTimestampToDate(matchUnmapped.createdAt), + }) ? match.chatCode : null, }; 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 37b3e7714..a33b30baa 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 @@ -2,6 +2,7 @@ import cachified from "@epic-web/cachified"; import type { LoaderFunctionArgs } from "react-router"; import { getUser } from "~/features/auth/core/user.server"; import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; +import { chatAccessible } from "~/features/chat/chat-utils"; 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"; @@ -125,6 +126,18 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { tournament.isOrganizerOrStreamer(user) || match.players.some((p) => p.id === user?.id); + const isStaff = user?.roles.includes("STAFF") ?? false; + const chatCodeExpired = tournament.ctx.isFinalized + ? true + : !chatAccessible({ + isStaff, + expiresAfterDays: 90, + comparedTo: tournament.ctx.startTime, + }); + + const visibleChatCode = + shouldSeeChat && !chatCodeExpired ? match.chatCode : undefined; + return { match: shouldSeeChat ? match : { ...match, chatCode: undefined }, results, @@ -132,6 +145,6 @@ export const loader = async ({ params }: LoaderFunctionArgs) => { matchIsOver, endedEarly, noScreen, - chatCode: shouldSeeChat ? match.chatCode : undefined, + chatCode: visibleChatCode, }; };