diff --git a/app/core/play/utils.ts b/app/core/play/utils.ts index 62cab9f76..6d708309c 100644 --- a/app/core/play/utils.ts +++ b/app/core/play/utils.ts @@ -123,13 +123,13 @@ export function otherGroupsForResponse({ lookingForMatch, ownGroup, }: { - groups: LFGGroup.FindLooking; + groups: LFGGroup.FindLookingAndOwnActive; likes: { given: Set; received: Set; }; lookingForMatch: boolean; - ownGroup: Unpacked; + ownGroup: Unpacked; }) { return groups .filter( diff --git a/app/models/LFGGroup.server.ts b/app/models/LFGGroup.server.ts index 87faf1381..1b554b846 100644 --- a/app/models/LFGGroup.server.ts +++ b/app/models/LFGGroup.server.ts @@ -16,8 +16,8 @@ export function create({ type, // TWIN starts looking immediately because it makes no sense // to pre-add players to the group - // looking: type === "TWIN", - looking: true, + // looking: type === "TWIN" ? "LOOKING" : "PRE-ADD", + status: "LOOKING", ranked, members: { create: { @@ -145,7 +145,7 @@ export async function matchUp({ }, data: { matchId: match.id, - looking: false, + status: "MATCH", }, }); } @@ -154,10 +154,10 @@ export function findById(id: string) { return db.lfgGroup.findUnique({ where: { id }, include: { members: true } }); } -export function findActiveByMember(user: { id: string }) { +export function findLookingByMember(user: { id: string }) { return db.lfgGroup.findFirst({ where: { - active: true, + status: "LOOKING", members: { some: { memberId: user.id, @@ -180,13 +180,33 @@ export function findActiveByMember(user: { id: string }) { }); } -export type FindLooking = Prisma.PromiseReturnType; -export function findLooking() { +export type FindLookingAndOwnActive = Prisma.PromiseReturnType< + typeof findLookingAndOwnActive +>; +export function findLookingAndOwnActive(userId?: string) { + const where = userId + ? { + OR: [ + { + status: "LOOKING" as const, + }, + { + members: { + some: { + memberId: userId, + }, + }, + NOT: { + status: "INACTIVE" as const, + }, + }, + ], + } + : { + status: "LOOKING" as const, + }; return db.lfgGroup.findMany({ - where: { - active: true, - looking: true, - }, + where, include: { members: { include: { @@ -217,7 +237,7 @@ export function startLooking(id: string) { id, }, data: { - looking: true, + status: "LOOKING", }, }); } @@ -228,8 +248,7 @@ export function setInactive(id: string) { id, }, data: { - looking: false, - active: false, + status: "INACTIVE", }, }); } diff --git a/app/models/LFGMatch.server.ts b/app/models/LFGMatch.server.ts index a86d63cbb..b34d96841 100644 --- a/app/models/LFGMatch.server.ts +++ b/app/models/LFGMatch.server.ts @@ -52,7 +52,6 @@ export async function reportScore({ const adjustedSkills = adjustSkills({ skills, playerIds }); - // https://stackoverflow.com/a/26715934 return db.$transaction([ db.skill.createMany({ data: adjustedSkills.map((s) => ({ ...s, matchId: UNSAFE_matchId })), @@ -64,9 +63,10 @@ export async function reportScore({ }, }, data: { - active: false, + status: "INACTIVE", }, }), + // https://stackoverflow.com/a/26715934 db.$executeRawUnsafe(` update "LfgGroupMatchStage" as lfg set "winnerGroupId" = lfg2.winner_id diff --git a/app/routes/play/add-players.tsx b/app/routes/play/add-players.tsx index ffddeee82..fcb57f151 100644 --- a/app/routes/play/add-players.tsx +++ b/app/routes/play/add-players.tsx @@ -16,6 +16,7 @@ import * as User from "~/models/User.server"; import { getUser, requireUser, validate } from "~/utils"; import styles from "~/styles/play-add-players.css"; import { isGroupAdmin } from "~/core/play/validators"; +import invariant from "tiny-invariant"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; @@ -24,7 +25,7 @@ export const links: LinksFunction = () => { export const action: ActionFunction = async ({ context }) => { const user = requireUser(context); - const group = await LFGGroup.findActiveByMember(user); + const group = await LFGGroup.findLookingByMember(user); validate(group, "Not a member of active group"); validate(isGroupAdmin({ user, group }), "Not captain of the group"); @@ -43,12 +44,15 @@ export const loader: LoaderFunction = async ({ context }) => { if (!user) return redirect("/play"); const [ownGroup, trustingUsers] = await Promise.all([ - LFGGroup.findActiveByMember(user), + LFGGroup.findLookingByMember(user), User.findTrusters(user.id), ]); if (!ownGroup) return redirect("/play"); - if (ownGroup.matchId) return redirect(`/play/match/${ownGroup.matchId}`); - if (ownGroup.looking) return redirect("/play/looking"); + if (ownGroup.status === "MATCH") { + invariant(ownGroup.matchId, "Unexpected no matchId but status is MATCH"); + return redirect(`/play/match/${ownGroup.matchId}`); + } + if (ownGroup.status === "LOOKING") return redirect("/play/looking"); return json({ inviteCode: ownGroup.inviteCode, diff --git a/app/routes/play/index.tsx b/app/routes/play/index.tsx index 830ce71a3..08da34c25 100644 --- a/app/routes/play/index.tsx +++ b/app/routes/play/index.tsx @@ -24,6 +24,7 @@ import * as LFGGroup from "~/models/LFGGroup.server"; import { Button } from "~/components/Button"; import { useUser } from "~/hooks/common"; import { filterExpiredGroups } from "~/core/play/utils"; +import invariant from "tiny-invariant"; export const links: LinksFunction = () => { return [{ rel: "stylesheet", href: styles }]; @@ -70,7 +71,7 @@ export const action: ActionFunction = async ({ request, context }) => { ranked: getRanked(), }); - if (group.looking) { + if (group.status === "LOOKING") { return redirect("/play/looking"); } @@ -92,7 +93,7 @@ export interface PlayFrontPageLoader { export const loader: LoaderFunction = async ({ context }) => { const user = getUser(context); - const groups = await LFGGroup.findLooking(); + const groups = await LFGGroup.findLookingAndOwnActive(user?.id); const counts = groups.filter(filterExpiredGroups).reduce( (acc: PlayFrontPageLoader["counts"], group) => { const memberCount = group.members.length; @@ -118,8 +119,11 @@ export const loader: LoaderFunction = async ({ context }) => { g.members.some((m) => m.user.id === user.id) ); if (!ownGroup) return json({ counts }); - if (ownGroup.matchId) return redirect(`/play/match/${ownGroup.matchId}`); - if (ownGroup.looking) return redirect("/play/looking"); + if (ownGroup.status === "MATCH") { + invariant(ownGroup.matchId, "Unexpected no matchId but status is MATCH"); + return redirect(`/play/match/${ownGroup.matchId}`); + } + if (ownGroup.status === "LOOKING") return redirect("/play/looking"); return redirect("/play/add-players"); }; diff --git a/app/routes/play/looking.tsx b/app/routes/play/looking.tsx index 226235bf8..2f92f3f85 100644 --- a/app/routes/play/looking.tsx +++ b/app/routes/play/looking.tsx @@ -84,9 +84,9 @@ export const action: ActionFunction = async ({ request, context }) => { }); const user = requireUser(context); - const ownGroup = await LFGGroup.findActiveByMember(user); + const ownGroup = await LFGGroup.findLookingByMember(user); validate(ownGroup, "No active group"); - validate(ownGroup.looking, "Group is not looking"); + validate(ownGroup.status === "LOOKING", "Group is not looking"); const validateIsGroupAdmin = () => validate(isGroupAdmin({ group: ownGroup, user }), "Not group admin"); @@ -108,7 +108,7 @@ export const action: ActionFunction = async ({ request, context }) => { if ( !groupToUniteWith || groupToUniteWith.members.length !== data.targetGroupSize || - !groupToUniteWith.looking + groupToUniteWith.status !== "LOOKING" ) { break; } @@ -140,15 +140,18 @@ export const action: ActionFunction = async ({ request, context }) => { validateIsGroupAdmin(); const groupToMatchUpWith = await LFGGroup.findById(data.targetGroupId); validate(groupToMatchUpWith, "Invalid targetGroupId"); - validate(!ownGroup.matchId, "Already matched up"); + validate(ownGroup.status === "LOOKING", "Group not looking"); // fail silently if already matched up or group stopped looking - if (groupToMatchUpWith.matchId || !groupToMatchUpWith.looking) break; + if (groupToMatchUpWith.status !== "LOOKING") { + break; + } await LFGGroup.matchUp({ groupIds: [ownGroup.id, data.targetGroupId], ranked: Boolean(groupToMatchUpWith.ranked && ownGroup.ranked), }); + // TODO: redirect here break; } case "UNLIKE": { @@ -220,15 +223,18 @@ export interface LookingLoaderData { export const loader: LoaderFunction = async ({ context }) => { const user = requireUser(context); - const groups = await LFGGroup.findLooking(); + const groups = await LFGGroup.findLookingAndOwnActive(user.id); const ownGroup = groups.find((g) => g.members.some((m) => m.user.id === user.id) ); if (!ownGroup) return redirect("/play"); - if (ownGroup.matchId) return redirect(`/play/match/${ownGroup.matchId}`); - if (!ownGroup.looking) return redirect("/play/add-players"); + if (ownGroup.status === "MATCH") { + invariant(ownGroup.matchId, "Unexpected no matchId but status is MATCH"); + return redirect(`/play/match/${ownGroup.matchId}`); + } + if (ownGroup.status === "PRE_ADD") return redirect("/play/add-players"); const lookingForMatch = ownGroup.type === "VERSUS" && diff --git a/app/routes/play/match.$id.tsx b/app/routes/play/match.$id.tsx index a175fdba5..3f9c6ca36 100644 --- a/app/routes/play/match.$id.tsx +++ b/app/routes/play/match.$id.tsx @@ -97,7 +97,7 @@ export const action: ActionFunction = async ({ g.members.some((m) => m.memberId === user.id) ); validate(ownGroup, "Not own match"); - validate(ownGroup.active, "Not active"); + validate(ownGroup.status === "MATCH", "Group doesn't have a match"); validate(isGroupAdmin({ group: ownGroup, user }), "Not group admin"); switch (data._action) { diff --git a/prisma/migrations/20220225161008_add_lfg_group_status/migration.sql b/prisma/migrations/20220225161008_add_lfg_group_status/migration.sql new file mode 100644 index 000000000..6af734284 --- /dev/null +++ b/prisma/migrations/20220225161008_add_lfg_group_status/migration.sql @@ -0,0 +1,23 @@ +/* + Warnings: + + - You are about to drop the column `active` on the `LfgGroup` table. All the data in the column will be lost. + - You are about to drop the column `looking` on the `LfgGroup` table. All the data in the column will be lost. + +*/ +-- CreateEnum +CREATE TYPE "LfgGroupStatus" AS ENUM ('PRE_ADD', 'LOOKING', 'MATCH', 'INACTIVE'); + +-- DropIndex +DROP INDEX "LfgGroup_active_idx"; + +-- DropIndex +DROP INDEX "LfgGroup_looking_idx"; + +-- AlterTable +ALTER TABLE "LfgGroup" DROP COLUMN "active", +DROP COLUMN "looking", +ADD COLUMN "status" "LfgGroupStatus" NOT NULL DEFAULT E'INACTIVE'; + +-- CreateIndex +CREATE INDEX "LfgGroup_status_idx" ON "LfgGroup"("status"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f9b36831f..731ec7eab 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -214,12 +214,18 @@ enum LfgGroupType { VERSUS } +enum LfgGroupStatus { + PRE_ADD + LOOKING + MATCH + INACTIVE +} + model LfgGroup { id String @id @default(uuid()) ranked Boolean? type LfgGroupType - active Boolean @default(true) - looking Boolean + status LfgGroupStatus @default(INACTIVE) matchId String? inviteCode String @default(uuid()) createdAt DateTime @default(now()) @@ -230,9 +236,8 @@ model LfgGroup { likesReceived LfgGroupLike[] @relation("target") matches LfgGroupMatchStage[] - @@index([active]) - @@index([looking]) @@index([type]) + @@index([status]) } model LfgGroupLike { diff --git a/prisma/seed/script.ts b/prisma/seed/script.ts index bfadc295e..24d36a653 100644 --- a/prisma/seed/script.ts +++ b/prisma/seed/script.ts @@ -389,8 +389,7 @@ export async function seed(variation?: SeedVariations) { const amountOfUsers = randomIntFromInterval(1, 3); await prisma.lfgGroup.create({ data: { - looking: true, - active: true, + status: "LOOKING", type: "VERSUS", ranked: i < 12, members: { @@ -408,8 +407,7 @@ export async function seed(variation?: SeedVariations) { for (let i = 0; i < 24; i++) { await prisma.lfgGroup.create({ data: { - looking: true, - active: true, + status: "LOOKING", type: "VERSUS", ranked: i < 12, members: { @@ -446,9 +444,8 @@ export async function seed(variation?: SeedVariations) { return prisma.lfgGroup.create({ data: { - looking: true, + status: "LOOKING", type: "VERSUS", - active: true, ranked: true, members: { createMany: {