Expiring chat codes

This commit is contained in:
Kalle 2026-03-18 20:56:53 +02:00
parent b9486c4f03
commit 1edb2809b5
6 changed files with 94 additions and 6 deletions

View File

@ -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", () => {

View File

@ -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 {

View File

@ -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<ChatContextValue | null>(null);
export function useChatContext(): ChatContextValue | null {

View File

@ -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:

View File

@ -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,
};

View File

@ -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,
};
};