mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-11-15 09:57:48 -06:00
Give every lobby set fresh team chats
This commit is contained in:
@@ -1426,9 +1426,6 @@ export interface Lobby {
|
||||
roomSize: LobbyRoomSize;
|
||||
/** Common chat used while pending and by spectators */
|
||||
chatRoomId: number | null;
|
||||
/** Team chat of the alpha side of the current set */
|
||||
alphaChatRoomId: number | null;
|
||||
bravoChatRoomId: number | null;
|
||||
/** Marker for the check-in opened notification having been sent */
|
||||
checkInNotifiedAt: number | null;
|
||||
/** Since when the started lobby has had no set being played, `null` while one is (or before the start) */
|
||||
@@ -1470,6 +1467,9 @@ export interface LobbySet {
|
||||
disputedAt: number | null;
|
||||
confirmedAt: number | null;
|
||||
confirmedByUserId: number | null;
|
||||
/** Team chat of the set's alpha side */
|
||||
alphaChatRoomId: number | null;
|
||||
bravoChatRoomId: number | null;
|
||||
createdAt: Generated<number>;
|
||||
}
|
||||
|
||||
|
||||
@@ -153,8 +153,8 @@ export async function findAllOpenRoomIdsByUserId(
|
||||
eb
|
||||
.case()
|
||||
.when("LobbySetParticipant.side", "=", "ALPHA")
|
||||
.then(eb.ref("Lobby.alphaChatRoomId"))
|
||||
.else(eb.ref("Lobby.bravoChatRoomId"))
|
||||
.then(eb.ref("LobbySet.alphaChatRoomId"))
|
||||
.else(eb.ref("LobbySet.bravoChatRoomId"))
|
||||
.end()
|
||||
.as("id"),
|
||||
)
|
||||
@@ -452,8 +452,8 @@ type ChatRoomOwnerColumn =
|
||||
| "TournamentTeam.chatRoomId"
|
||||
| "ScrimPost.chatRoomId"
|
||||
| "Lobby.chatRoomId"
|
||||
| "Lobby.alphaChatRoomId"
|
||||
| "Lobby.bravoChatRoomId";
|
||||
| "LobbySet.alphaChatRoomId"
|
||||
| "LobbySet.bravoChatRoomId";
|
||||
|
||||
/** The one table that can own a room of each type, and the columns of it that point to the room. */
|
||||
const OWNER_COLUMNS_BY_ROOM_TYPE = {
|
||||
@@ -463,7 +463,7 @@ const OWNER_COLUMNS_BY_ROOM_TYPE = {
|
||||
TOURNAMENT_TEAM: ["TournamentTeam.chatRoomId"],
|
||||
SCRIM: ["ScrimPost.chatRoomId"],
|
||||
LOBBY: ["Lobby.chatRoomId"],
|
||||
LOBBY_TEAM: ["Lobby.alphaChatRoomId", "Lobby.bravoChatRoomId"],
|
||||
LOBBY_TEAM: ["LobbySet.alphaChatRoomId", "LobbySet.bravoChatRoomId"],
|
||||
} as const satisfies Record<ChatRoomType, ReadonlyArray<ChatRoomOwnerColumn>>;
|
||||
|
||||
function noOwner(
|
||||
|
||||
@@ -31,9 +31,6 @@ describe("findAllTeamChatsByChatRoomIds", () => {
|
||||
);
|
||||
await LobbyProgression.advance(lobby.id);
|
||||
const [set] = await LobbyMatchRepository.findAllSetsByLobbyId(lobby.id);
|
||||
const { alphaChatRoomId } = (await withUserId(hostId(), () =>
|
||||
LobbyRepository.findById(lobby.id),
|
||||
))!;
|
||||
const [leaverId, ...stayingIds] = LobbySet.playersOf(set, "ALPHA");
|
||||
// a set without a map reported would be called off
|
||||
await withUserId(leaverId, () =>
|
||||
@@ -51,11 +48,45 @@ describe("findAllTeamChatsByChatRoomIds", () => {
|
||||
});
|
||||
|
||||
const [teamChat] = await LobbyMatchRepository.findAllTeamChatsByChatRoomIds(
|
||||
[alphaChatRoomId!],
|
||||
[set.alphaChatRoomId!],
|
||||
);
|
||||
expect(teamChat.userIds).toEqual(expect.arrayContaining(stayingIds));
|
||||
expect(teamChat.userIds).toHaveLength(stayingIds.length);
|
||||
});
|
||||
|
||||
test("every set has its own team chats, the finished set's inactive", async () => {
|
||||
const lobby = await LobbyFactory.create(
|
||||
{
|
||||
hostUserId: hostId(),
|
||||
startsAt: dateToDatabaseTimestamp(subMinutes(new Date(), 1)),
|
||||
},
|
||||
{
|
||||
signedUpUserIds: users.ids(8).slice(1),
|
||||
checkedInUserIds: users.ids(8),
|
||||
playedSetCount: 1,
|
||||
},
|
||||
);
|
||||
const [previousSet, currentSet] =
|
||||
await LobbyMatchRepository.findAllSetsByLobbyId(lobby.id);
|
||||
|
||||
const teamChats = await LobbyMatchRepository.findAllTeamChatsByChatRoomIds([
|
||||
previousSet.alphaChatRoomId!,
|
||||
currentSet.alphaChatRoomId!,
|
||||
]);
|
||||
|
||||
expect(currentSet.alphaChatRoomId).not.toBe(previousSet.alphaChatRoomId);
|
||||
expect(
|
||||
teamChats.map(({ chatRoomId, isInactive }) => ({
|
||||
chatRoomId,
|
||||
isInactive,
|
||||
})),
|
||||
).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ chatRoomId: previousSet.alphaChatRoomId, isInactive: true },
|
||||
{ chatRoomId: currentSet.alphaChatRoomId, isInactive: false },
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("confirmReadyChecks", () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Transaction } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
import type { DB, Tables } from "~/db/tables";
|
||||
import { actorId } from "~/features/auth/core/user.server";
|
||||
import * as ChatRepository from "~/features/chat/ChatRepository.server";
|
||||
import * as ScannerIngestRepository from "~/features/scanner-ingest/ScannerIngestRepository.server";
|
||||
import * as ReportedWeaponRepository from "~/features/sendouq-match/ReportedWeaponRepository.server";
|
||||
import type { ModeWithStage } from "~/modules/in-game-lists/types";
|
||||
@@ -10,6 +11,7 @@ import { commonUserSelect, jsonArrayFrom } from "~/utils/kysely.server";
|
||||
import type { Unwrapped } from "~/utils/types";
|
||||
import * as LobbySet from "./core/LobbySet";
|
||||
import type { LobbySide } from "./lobbies-constants";
|
||||
import { lobbyChatRoomExpiresAt } from "./lobbies-utils";
|
||||
|
||||
/** A set of the lobby with its games and participants, see {@link findAllSetsByLobbyId}. */
|
||||
export type LobbySetWithGames = Unwrapped<typeof findAllSetsByLobbyId>;
|
||||
@@ -24,6 +26,8 @@ export function findAllSetsByLobbyId(lobbyId: number) {
|
||||
"LobbySet.forfeitingSide",
|
||||
"LobbySet.disputedAt",
|
||||
"LobbySet.confirmedAt",
|
||||
"LobbySet.alphaChatRoomId",
|
||||
"LobbySet.bravoChatRoomId",
|
||||
"LobbySet.createdAt",
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
@@ -59,32 +63,28 @@ export function findAllSetsByLobbyId(lobbyId: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Lobbies owning the given team chat rooms, with the players of the side the chat room is for in the latest
|
||||
* set who are still in the room: team chats follow the current set.
|
||||
* Sets owning the given team chat rooms, with the players of the side the chat room is for who are still in the
|
||||
* lobby's room. A finished set's team chats are inactive.
|
||||
*/
|
||||
export async function findAllTeamChatsByChatRoomIds(chatRoomIds: number[]) {
|
||||
if (chatRoomIds.length === 0) return [];
|
||||
|
||||
const lobbies = await db
|
||||
.selectFrom("Lobby")
|
||||
const sets = await db
|
||||
.selectFrom("LobbySet")
|
||||
.innerJoin("Lobby", "Lobby.id", "LobbySet.lobbyId")
|
||||
.select((eb) => [
|
||||
"Lobby.id",
|
||||
"Lobby.name",
|
||||
"Lobby.alphaChatRoomId",
|
||||
"Lobby.bravoChatRoomId",
|
||||
"Lobby.id as lobbyId",
|
||||
"Lobby.name as lobbyName",
|
||||
"Lobby.endedAt",
|
||||
"LobbySet.alphaChatRoomId",
|
||||
"LobbySet.bravoChatRoomId",
|
||||
"LobbySet.confirmedAt",
|
||||
"LobbySet.forfeitingSide",
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("LobbySetParticipant")
|
||||
.select(["LobbySetParticipant.userId", "LobbySetParticipant.side"])
|
||||
.where(
|
||||
"LobbySetParticipant.setId",
|
||||
"=",
|
||||
eb
|
||||
.selectFrom("LobbySet")
|
||||
.select((innerEb) => innerEb.fn.max("LobbySet.id").as("id"))
|
||||
.whereRef("LobbySet.lobbyId", "=", "Lobby.id"),
|
||||
)
|
||||
.whereRef("LobbySetParticipant.setId", "=", "LobbySet.id")
|
||||
.where((innerEb) =>
|
||||
innerEb.exists(
|
||||
innerEb
|
||||
@@ -99,30 +99,33 @@ export async function findAllTeamChatsByChatRoomIds(chatRoomIds: number[]) {
|
||||
.where("LobbyParticipant.status", "=", "IN_ROOM"),
|
||||
),
|
||||
),
|
||||
).as("latestSetParticipants"),
|
||||
).as("participants"),
|
||||
])
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb("Lobby.alphaChatRoomId", "in", chatRoomIds),
|
||||
eb("Lobby.bravoChatRoomId", "in", chatRoomIds),
|
||||
eb("LobbySet.alphaChatRoomId", "in", chatRoomIds),
|
||||
eb("LobbySet.bravoChatRoomId", "in", chatRoomIds),
|
||||
]),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return lobbies.flatMap((lobby) =>
|
||||
return sets.flatMap((set) =>
|
||||
(["ALPHA", "BRAVO"] as const).flatMap((side) => {
|
||||
const chatRoomId =
|
||||
side === "ALPHA" ? lobby.alphaChatRoomId : lobby.bravoChatRoomId;
|
||||
side === "ALPHA" ? set.alphaChatRoomId : set.bravoChatRoomId;
|
||||
if (chatRoomId === null || !chatRoomIds.includes(chatRoomId)) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
chatRoomId,
|
||||
side,
|
||||
lobbyId: lobby.id,
|
||||
lobbyName: lobby.name,
|
||||
isInactive: lobby.endedAt !== null,
|
||||
userIds: lobby.latestSetParticipants
|
||||
lobbyId: set.lobbyId,
|
||||
lobbyName: set.lobbyName,
|
||||
isInactive:
|
||||
set.endedAt !== null ||
|
||||
set.confirmedAt !== null ||
|
||||
set.forfeitingSide !== null,
|
||||
userIds: set.participants
|
||||
.filter((participant) => participant.side === side)
|
||||
.map((participant) => participant.userId),
|
||||
},
|
||||
@@ -132,7 +135,7 @@ export async function findAllTeamChatsByChatRoomIds(chatRoomIds: number[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a set in the room: who plays on which side (the rest sit out) and the maps. Returns the set id.
|
||||
* Starts a set in the room: who plays on which side (the rest sit out), the maps and fresh team chats. Returns the set id.
|
||||
*/
|
||||
export function insertSet({
|
||||
lobbyId,
|
||||
@@ -150,9 +153,23 @@ export function insertSet({
|
||||
sittingOut: number[];
|
||||
}) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const lobby = await trx
|
||||
.selectFrom("Lobby")
|
||||
.select("Lobby.startsAt")
|
||||
.where("Lobby.id", "=", lobbyId)
|
||||
.executeTakeFirstOrThrow();
|
||||
const [alphaChatRoomId, bravoChatRoomId] = await ChatRepository.insertRooms(
|
||||
{
|
||||
type: "LOBBY_TEAM",
|
||||
expiresAt: lobbyChatRoomExpiresAt(lobby.startsAt),
|
||||
count: 2,
|
||||
},
|
||||
trx,
|
||||
);
|
||||
|
||||
const set = await trx
|
||||
.insertInto("LobbySet")
|
||||
.values({ lobbyId, bestOf })
|
||||
.values({ lobbyId, bestOf, alphaChatRoomId, bravoChatRoomId })
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { addHours } from "date-fns";
|
||||
import type { ExpressionBuilder, NotNull, Transaction } from "kysely";
|
||||
import { sql } from "kysely";
|
||||
import { db } from "~/db/sql";
|
||||
@@ -7,7 +6,7 @@ import type { AssociationVisibility } from "~/features/associations/associations
|
||||
import * as Visibility from "~/features/associations/core/Visibility";
|
||||
import { actorIdOrNull } from "~/features/auth/core/user.server";
|
||||
import * as ChatRepository from "~/features/chat/ChatRepository.server";
|
||||
import { databaseTimestampNow, databaseTimestampToDate } from "~/utils/dates";
|
||||
import { databaseTimestampNow } from "~/utils/dates";
|
||||
import { shortNanoid } from "~/utils/id";
|
||||
import {
|
||||
commonUserSelect,
|
||||
@@ -20,10 +19,10 @@ import { toDBBoolean } from "~/utils/sql";
|
||||
import type { Unwrapped } from "~/utils/types";
|
||||
import * as LobbyMatchRepository from "./LobbyMatchRepository.server";
|
||||
import {
|
||||
CHAT_ROOM_LIFESPAN_HOURS,
|
||||
CHECK_IN_LEAD_MINUTES,
|
||||
type LobbyParticipantStatus,
|
||||
} from "./lobbies-constants";
|
||||
import { lobbyChatRoomExpiresAt } from "./lobbies-utils";
|
||||
|
||||
const INDEX_LIST_LIMIT = 50;
|
||||
|
||||
@@ -61,8 +60,6 @@ export async function findById(
|
||||
"Lobby.modes",
|
||||
"Lobby.roomSize",
|
||||
"Lobby.chatRoomId",
|
||||
"Lobby.alphaChatRoomId",
|
||||
"Lobby.bravoChatRoomId",
|
||||
"Lobby.hostUserId",
|
||||
"Lobby.pendingSince",
|
||||
"Lobby.endedAt",
|
||||
@@ -591,7 +588,7 @@ export async function findAllUnendedIdsCheckInOpened() {
|
||||
|
||||
type InsertArgs = Omit<
|
||||
TablesInsertable["Lobby"],
|
||||
"chatRoomId" | "alphaChatRoomId" | "bravoChatRoomId" | "modes" | "shareKey"
|
||||
"chatRoomId" | "modes" | "shareKey"
|
||||
> & {
|
||||
modes: Tables["Lobby"]["modes"];
|
||||
/** `LINK_ONLY` = seen only through the share link, by those not participating */
|
||||
@@ -600,7 +597,7 @@ type InsertArgs = Omit<
|
||||
hostStatus?: Extract<LobbyParticipantStatus, "REGISTERED" | "CHECKED_IN">;
|
||||
};
|
||||
|
||||
/** Inserts the lobby with its chat rooms and visibility, the host as its first participant. Returns the id. */
|
||||
/** Inserts the lobby with its chat room and visibility, the host as its first participant. Returns the id. */
|
||||
export function insert({
|
||||
visibility,
|
||||
modes,
|
||||
@@ -608,16 +605,8 @@ export function insert({
|
||||
...args
|
||||
}: InsertArgs) {
|
||||
return db.transaction().execute(async (trx) => {
|
||||
const expiresAt = addHours(
|
||||
databaseTimestampToDate(args.startsAt),
|
||||
CHAT_ROOM_LIFESPAN_HOURS,
|
||||
);
|
||||
const chatRoom = await ChatRepository.insertRoom(
|
||||
{ type: "LOBBY", expiresAt },
|
||||
trx,
|
||||
);
|
||||
const [alphaChatRoomId, bravoChatRoomId] = await ChatRepository.insertRooms(
|
||||
{ type: "LOBBY_TEAM", expiresAt, count: 2 },
|
||||
{ type: "LOBBY", expiresAt: lobbyChatRoomExpiresAt(args.startsAt) },
|
||||
trx,
|
||||
);
|
||||
|
||||
@@ -627,8 +616,6 @@ export function insert({
|
||||
...args,
|
||||
modes: JSON.stringify(modes),
|
||||
chatRoomId: chatRoom.id,
|
||||
alphaChatRoomId,
|
||||
bravoChatRoomId,
|
||||
shareKey: visibility === "LINK_ONLY" ? shortNanoid() : null,
|
||||
})
|
||||
.returning(["id", "createdAt"])
|
||||
@@ -825,16 +812,20 @@ export function end(id: number) {
|
||||
|
||||
const lobby = await trx
|
||||
.selectFrom("Lobby")
|
||||
.select([
|
||||
"Lobby.chatRoomId",
|
||||
"Lobby.alphaChatRoomId",
|
||||
"Lobby.bravoChatRoomId",
|
||||
])
|
||||
.select("Lobby.chatRoomId")
|
||||
.where("Lobby.id", "=", id)
|
||||
.executeTakeFirstOrThrow();
|
||||
const sets = await trx
|
||||
.selectFrom("LobbySet")
|
||||
.select(["LobbySet.alphaChatRoomId", "LobbySet.bravoChatRoomId"])
|
||||
.where("LobbySet.lobbyId", "=", id)
|
||||
.execute();
|
||||
|
||||
await ChatRepository.updateRoomsInactive(
|
||||
[lobby.chatRoomId, lobby.alphaChatRoomId, lobby.bravoChatRoomId],
|
||||
[
|
||||
lobby.chatRoomId,
|
||||
...sets.flatMap((set) => [set.alphaChatRoomId, set.bravoChatRoomId]),
|
||||
],
|
||||
true,
|
||||
trx,
|
||||
);
|
||||
|
||||
@@ -81,8 +81,8 @@ export const loader = async ({ params, request }: LoaderFunctionArgs) => {
|
||||
|
||||
const viewerTeamChatRoomId = isPlayingUnfinishedSet
|
||||
? viewerSide === "ALPHA"
|
||||
? lobby.alphaChatRoomId
|
||||
: lobby.bravoChatRoomId
|
||||
? latestSet.alphaChatRoomId
|
||||
: latestSet.bravoChatRoomId
|
||||
: null;
|
||||
|
||||
const userIds = R.unique([
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { addHours } from "date-fns";
|
||||
import { CHANNEL_PREFIX } from "~/features/events/events-types";
|
||||
import { databaseTimestampToDate } from "~/utils/dates";
|
||||
import { lobbyPage, SENDOU_INK_BASE_URL } from "~/utils/urls";
|
||||
import { CHAT_ROOM_LIFESPAN_HOURS } from "./lobbies-constants";
|
||||
import { lobbySearchParams } from "./lobbies-search-params";
|
||||
|
||||
/** Topic the lobby's page revalidates on: every change to its participants and sets. */
|
||||
@@ -15,3 +18,7 @@ export const lobbyShareLink = ({
|
||||
shareKey: string;
|
||||
}) =>
|
||||
`${SENDOU_INK_BASE_URL}${lobbySearchParams.href(lobbyPage(lobbyId), { key: shareKey })}`;
|
||||
|
||||
/** When the lobby's chat rooms, its own and its sets' team chats, expire. */
|
||||
export const lobbyChatRoomExpiresAt = (startsAt: number) =>
|
||||
addHours(databaseTimestampToDate(startsAt), CHAT_ROOM_LIFESPAN_HOURS);
|
||||
|
||||
@@ -49,8 +49,8 @@ export default function LobbyPage() {
|
||||
useVoiceFollow({
|
||||
roomIds: [
|
||||
data.lobby.chatRoomId,
|
||||
data.lobby.alphaChatRoomId,
|
||||
data.lobby.bravoChatRoomId,
|
||||
data.sets.at(-1)?.alphaChatRoomId ?? null,
|
||||
data.sets.at(-1)?.bravoChatRoomId ?? null,
|
||||
].filter((id) => id !== null),
|
||||
targetRoomId: data.voiceRoomId,
|
||||
});
|
||||
|
||||
@@ -768,8 +768,8 @@ describe("MapList.generate() with history", () => {
|
||||
return gen.next({ amount }).value;
|
||||
}
|
||||
|
||||
function szHistory(stageIds: StageId[]): ModeWithStage[] {
|
||||
return stageIds.map((stageId) => ({ mode: "SZ", stageId }));
|
||||
function szHistory(historyStageIds: StageId[]): ModeWithStage[] {
|
||||
return historyStageIds.map((stageId) => ({ mode: "SZ", stageId }));
|
||||
}
|
||||
|
||||
test("does not start with the last played mode", () => {
|
||||
|
||||
@@ -46,8 +46,8 @@ export function useVoiceRoom(room: {
|
||||
|
||||
/**
|
||||
* Keeps the user's call following `targetRoomId` among related rooms: when the target changes (say the user's
|
||||
* team for a lobby's next set) a call in any of `roomIds` moves to it. Joining the target in the first place
|
||||
* stays the user's choice, and so does switching between the rooms while the target holds.
|
||||
* team for a lobby's next set) a call in any of `roomIds`, or in the previous target, moves to it. Joining the
|
||||
* target in the first place stays the user's choice, and so does switching between the rooms while the target holds.
|
||||
*/
|
||||
export function useVoiceFollow({
|
||||
roomIds,
|
||||
@@ -66,7 +66,11 @@ export function useVoiceFollow({
|
||||
if (targetRoomId === null) return;
|
||||
|
||||
void voiceClient.follow({
|
||||
roomIds: roomIdsKey.split(",").map(Number),
|
||||
// the previous target may be gone from `roomIds`, like a lobby's previous set's team chat
|
||||
roomIds: [
|
||||
...roomIdsKey.split(",").map(Number),
|
||||
...(followedRoomId === null ? [] : [followedRoomId]),
|
||||
],
|
||||
targetRoomId,
|
||||
});
|
||||
}, [targetRoomId, followedRoomId, roomIdsKey]);
|
||||
|
||||
@@ -1338,7 +1338,8 @@ test.describe("Lobbies", () => {
|
||||
|
||||
/**
|
||||
* A lobby has a chat for everyone in it and one per team, a team's chat reaching only its
|
||||
* players. The lobby page opens a player's two chats side by side.
|
||||
* players. Every set starts with fresh team chats while the lobby's chat carries on. The
|
||||
* lobby page opens a player's two chats side by side.
|
||||
*/
|
||||
test.describe("Chat", () => {
|
||||
test("a team's chat reaches only its players", async ({
|
||||
@@ -1397,6 +1398,85 @@ test.describe("Lobbies", () => {
|
||||
await expect(chat.locators.roomRows).toContainText("Lobby · Team");
|
||||
});
|
||||
});
|
||||
|
||||
test("every set starts with fresh team chats", async ({
|
||||
page,
|
||||
factories,
|
||||
}) => {
|
||||
const { lobby, players, alphaPlayers, bravoPlayers } = await startedLobby(
|
||||
page,
|
||||
factories,
|
||||
);
|
||||
const sender = alphaPlayers[0];
|
||||
const lobbyPage = new LobbyPage(page);
|
||||
const chat = new ChatSidebar(page);
|
||||
|
||||
await test.step("An Alpha player writes to their team and to the lobby in the first set", async () => {
|
||||
await impersonate(page, sender.id);
|
||||
await lobbyPage.goto(lobby.id);
|
||||
|
||||
await chat.selectRoomTab("Team");
|
||||
await chat.chat().send("first set plan");
|
||||
await expect(chat.chat().message("first set plan")).toBeVisible();
|
||||
|
||||
await chat.selectRoomTab("Lobby");
|
||||
await chat.chat().send("hello lobby");
|
||||
await expect(chat.chat().message("hello lobby")).toBeVisible();
|
||||
});
|
||||
|
||||
await playSet(page, {
|
||||
lobbyId: lobby.id,
|
||||
alphaPlayer: sender,
|
||||
bravoPlayer: bravoPlayers[0],
|
||||
});
|
||||
|
||||
await expect(lobbyPage.locators.reportScoreButton).toBeVisible();
|
||||
const nextAlphaIds = await rosterIds(lobbyPage, "Alpha", players);
|
||||
const senderTeamIds = nextAlphaIds.includes(sender.id)
|
||||
? nextAlphaIds
|
||||
: idsOf(players).filter((id) => !nextAlphaIds.includes(id));
|
||||
const newTeammate = players.find(
|
||||
(player) =>
|
||||
player.id !== sender.id && senderTeamIds.includes(player.id),
|
||||
)!;
|
||||
// the teams were reshuffled, so someone of the first set's team now plays against the sender
|
||||
const formerTeammate = alphaPlayers.find(
|
||||
(player) => !senderTeamIds.includes(player.id),
|
||||
)!;
|
||||
|
||||
await test.step("The sender's team chat of the next set starts empty", async () => {
|
||||
await impersonate(page, sender.id);
|
||||
await lobbyPage.goto(lobby.id);
|
||||
|
||||
await chat.selectRoomTab("Team");
|
||||
await chat.chat().send("second set plan");
|
||||
await expect(chat.chat().message("second set plan")).toBeVisible();
|
||||
await expect(chat.chat().message("first set plan")).toHaveCount(0);
|
||||
|
||||
await chat.selectRoomTab("Lobby");
|
||||
await expect(chat.chat().message("hello lobby")).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Their new teammate reads only the next set's message", async () => {
|
||||
await impersonate(page, newTeammate.id);
|
||||
await lobbyPage.goto(lobby.id);
|
||||
|
||||
await chat.selectRoomTab("Team");
|
||||
await expect(chat.chat().message("second set plan")).toBeVisible();
|
||||
await expect(chat.chat().message("first set plan")).toHaveCount(0);
|
||||
});
|
||||
|
||||
await test.step("A former teammate now on the other team reads neither", async () => {
|
||||
await impersonate(page, formerTeammate.id);
|
||||
await lobbyPage.goto(lobby.id);
|
||||
|
||||
await chat.selectRoomTab("Team");
|
||||
await chat.chat().send("other team plan");
|
||||
await expect(chat.chat().message("other team plan")).toBeVisible();
|
||||
await expect(chat.chat().message("second set plan")).toHaveCount(0);
|
||||
await expect(chat.chat().message("first set plan")).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
64
migrations/20261027183901-lobby-set-team-chat-rooms.ts
Normal file
64
migrations/20261027183901-lobby-set-team-chat-rooms.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { type Kysely, sql } from "kysely";
|
||||
|
||||
/** Moves the lobby team chat rooms from `Lobby` to `LobbySet`, so every set starts with fresh team chats. */
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.transaction().execute(async (trx) => {
|
||||
await trx.schema
|
||||
.alterTable("LobbySet")
|
||||
.addColumn("alphaChatRoomId", "integer", (col) =>
|
||||
col.references("ChatRoom.id").onDelete("set null"),
|
||||
)
|
||||
.execute();
|
||||
await trx.schema
|
||||
.alterTable("LobbySet")
|
||||
.addColumn("bravoChatRoomId", "integer", (col) =>
|
||||
col.references("ChatRoom.id").onDelete("set null"),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await sql`
|
||||
update "LobbySet"
|
||||
set
|
||||
"alphaChatRoomId" = (select "alphaChatRoomId" from "Lobby" where "Lobby"."id" = "LobbySet"."lobbyId"),
|
||||
"bravoChatRoomId" = (select "bravoChatRoomId" from "Lobby" where "Lobby"."id" = "LobbySet"."lobbyId")
|
||||
where "LobbySet"."id" = (
|
||||
select max("Latest"."id") from "LobbySet" as "Latest" where "Latest"."lobbyId" = "LobbySet"."lobbyId"
|
||||
)
|
||||
`.execute(trx);
|
||||
|
||||
for (const column of ["alphaChatRoomId", "bravoChatRoomId"]) {
|
||||
await sql
|
||||
.raw(`drop trigger "lobby_${snakeCase(column)}_deletes_chat_room"`)
|
||||
.execute(trx);
|
||||
await trx.schema.dropIndex(`lobby_${snakeCase(column)}`).execute();
|
||||
await trx.schema.alterTable("Lobby").dropColumn(column).execute();
|
||||
|
||||
await trx.schema
|
||||
.createIndex(`lobby_set_${snakeCase(column)}`)
|
||||
.on("LobbySet")
|
||||
.column(column)
|
||||
.unique()
|
||||
.where(sql.ref(column), "is not", null)
|
||||
.execute();
|
||||
await sql
|
||||
.raw(
|
||||
`create trigger "lobby_set_${snakeCase(column)}_deletes_chat_room" after delete on "LobbySet"
|
||||
when old."${column}" is not null
|
||||
begin delete from "ChatRoom" where "id" = old."${column}"; end`,
|
||||
)
|
||||
.execute(trx);
|
||||
}
|
||||
|
||||
// team rooms of lobbies that never got to a set
|
||||
await sql`
|
||||
delete from "ChatRoom"
|
||||
where "type" = 'LOBBY_TEAM'
|
||||
and not exists (select 1 from "LobbySet" where "alphaChatRoomId" = "ChatRoom"."id")
|
||||
and not exists (select 1 from "LobbySet" where "bravoChatRoomId" = "ChatRoom"."id")
|
||||
`.execute(trx);
|
||||
});
|
||||
}
|
||||
|
||||
function snakeCase(value: string) {
|
||||
return value.replace(/([a-z])([A-Z])/g, "$1_$2").toLowerCase();
|
||||
}
|
||||
Reference in New Issue
Block a user