diff --git a/app/db/seed/factories/SQGroupFactory.ts b/app/db/seed/factories/SQGroupFactory.ts index 7915dc8a2..3c8516468 100644 --- a/app/db/seed/factories/SQGroupFactory.ts +++ b/app/db/seed/factories/SQGroupFactory.ts @@ -7,19 +7,19 @@ type InsertArgs = Omit< Parameters[0], "userId" > & { - /** The group's members, the first of them its owner. */ + /** The group's members, the first of them its creator. */ memberUserIds: number[]; }; type Options = { /** Was the group made in the matchmaking UI? */ isMatchmade?: boolean; - /** Groups that have liked this one, each of them as its own owner. */ + /** Groups that have liked this one. */ likedByGroupIds?: number[]; }; /** - * Creates SendouQ groups. The first of `memberUserIds` is the owner, whose + * Creates SendouQ groups. The first of `memberUserIds` is the creator, whose * membership the repository creates with the group; the rest join it the way they do * in production. Invite and chat codes are the repository's own. */ @@ -28,19 +28,19 @@ export const { create } = defineFactory({ status: "ACTIVE" as const, }), insert: async ({ memberUserIds, ...args }: InsertArgs) => { - const [ownerUserId, ...otherMemberUserIds] = memberUserIds; - invariant(ownerUserId, "A group needs at least an owner"); + const [creatorUserId, ...otherMemberUserIds] = memberUserIds; + invariant(creatorUserId, "A group needs at least one member"); const group = await SQGroupRepository.insert({ ...args, - userId: ownerUserId, + userId: creatorUserId, }); for (const userId of otherMemberUserIds) { await SQGroupRepository.insertMember(group.id, { userId }); } - return { id: group.id, memberUserIds, ownerUserId }; + return { id: group.id, memberUserIds }; }, applyOptions: async (group, { isMatchmade, likedByGroupIds }: Options) => { for (const likerGroupId of likedByGroupIds ?? []) { diff --git a/app/db/seed/factories/SQMatchFactory.ts b/app/db/seed/factories/SQMatchFactory.ts index 0299ddec0..5c0f51598 100644 --- a/app/db/seed/factories/SQMatchFactory.ts +++ b/app/db/seed/factories/SQMatchFactory.ts @@ -139,7 +139,7 @@ async function cancelMatch( ) { const request = await SQMatchRepository.requestCancelMatch({ matchId: match.id, - requestedByUserId: match.alphaGroup.ownerUserId, + requestedByUserId: match.alphaGroup.memberUserIds[0], ...requested, }); @@ -150,7 +150,7 @@ async function cancelMatch( const acceptance = await SQMatchRepository.acceptCancelMatch({ matchId: match.id, - acceptedByUserId: match.bravoGroup.ownerUserId, + acceptedByUserId: match.bravoGroup.memberUserIds[0], ...accepted, }); diff --git a/app/db/tables.ts b/app/db/tables.ts index 68fcfa3e8..c44f7bd04 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -368,7 +368,6 @@ export interface GroupMember { createdAt: Generated; groupId: number; note: string | null; - role: "OWNER" | "MANAGER" | "REGULAR"; userId: number; } diff --git a/app/features/sendouq-match/SQMatchRepository.server.ts b/app/features/sendouq-match/SQMatchRepository.server.ts index e9ef28fea..48bfbb8fc 100644 --- a/app/features/sendouq-match/SQMatchRepository.server.ts +++ b/app/features/sendouq-match/SQMatchRepository.server.ts @@ -154,7 +154,6 @@ function groupWithTeamAndMembers( ) .select((arrayEb) => [ ...commonUserSelect(arrayEb), - "GroupMember.role", "GroupMember.note", "User.inGameName", "User.vc", diff --git a/app/features/sendouq-match/actions/q.match.$id.server.ts b/app/features/sendouq-match/actions/q.match.$id.server.ts index 3f14a88ec..dbee3f5f1 100644 --- a/app/features/sendouq-match/actions/q.match.$id.server.ts +++ b/app/features/sendouq-match/actions/q.match.$id.server.ts @@ -134,10 +134,9 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { "This group must use the continue vote", ); - const requester = previousGroup.members.find((m) => m.id === user.id); errorToastIfFalsy( - requester?.role === "OWNER", - "You are not the owner of the group", + previousGroup.members.some((m) => m.id === user.id), + "Not a member of the group", ); for (const member of previousGroup.members) { @@ -147,10 +146,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { await SQGroupRepository.insertFromPrevious({ previousGroupId: data.previousGroupId, - members: previousGroup.members.map((m) => ({ - id: m.id, - role: m.role, - })), + memberUserIds: previousGroup.members.map((m) => m.id), status: "ACTIVE", }); @@ -226,14 +222,14 @@ export const action = async ({ request, params }: ActionFunctionArgs) => { }); if (votingResult?.type === "RESOLVED") { - const survivors = viewerGroup.members - .filter((m) => votingResult.continuingUserIds.includes(m.id)) - .map((m) => ({ id: m.id, role: m.role })); + const survivors = viewerGroup.members.filter((m) => + votingResult.continuingUserIds.includes(m.id), + ); try { await SQGroupRepository.insertFromPrevious({ previousGroupId: viewerGroup.id, - members: survivors, + memberUserIds: survivors.map((m) => m.id), status: "ACTIVE", }); } catch (error) { diff --git a/app/features/sendouq-match/components/RejoinSections.tsx b/app/features/sendouq-match/components/RejoinSections.tsx index 31e52b96b..f211ac089 100644 --- a/app/features/sendouq-match/components/RejoinSections.tsx +++ b/app/features/sendouq-match/components/RejoinSections.tsx @@ -56,39 +56,26 @@ export function MatchmadeRejoinSection({ export function TrustedRejoinSection({ viewerGroup, - viewerUserId, }: { viewerGroup: NonNullable; - viewerUserId: number; }) { const { t } = useTranslation(["q"]); - const viewerRole = viewerGroup.members.find( - (m) => m.id === viewerUserId, - )?.role; const lookAgain = useActionSubmit(matchSchema); - if (viewerRole === "OWNER") { - return ( -
- { - lookAgain.submit("LOOK_AGAIN", { - previousGroupId: viewerGroup.id, - }); - }} - > - {t("q:match.actions.lookAgain")} - -
- ); - } - return ( -

- {t("q:match.rematch.waitingCaptain")} -

+
+ { + lookAgain.submit("LOOK_AGAIN", { + previousGroupId: viewerGroup.id, + }); + }} + > + {t("q:match.actions.lookAgain")} + +
); } diff --git a/app/features/sendouq-match/components/SendouQMatchActionTab.tsx b/app/features/sendouq-match/components/SendouQMatchActionTab.tsx index bc727b3df..1d1bb502c 100644 --- a/app/features/sendouq-match/components/SendouQMatchActionTab.tsx +++ b/app/features/sendouq-match/components/SendouQMatchActionTab.tsx @@ -274,10 +274,7 @@ function RequeueTab({ {!data.isOffSeason && !viewerGroup.matchmade && (!awaitingConfirmation || isOnReporterTeam) ? ( - + ) : null} {isOnReporterTeam ?
: null} diff --git a/app/features/sendouq/SQGroupRepository.server.ts b/app/features/sendouq/SQGroupRepository.server.ts index 4ae632291..67e43d5c8 100644 --- a/app/features/sendouq/SQGroupRepository.server.ts +++ b/app/features/sendouq/SQGroupRepository.server.ts @@ -7,7 +7,6 @@ import type { UserMapModePreferences } from "~/db/tables-json"; import { actorId } from "~/features/auth/core/user.server"; import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates"; import { shortNanoid } from "~/utils/id"; -import invariant from "~/utils/invariant"; import { commonUserMembersAgg, commonUserSelect, @@ -77,7 +76,6 @@ export async function findCurrentGroups() { commonUserMembersAgg(eb, { mapModePreferences: eb.ref("User.mapModePreferences"), noScreen: eb.ref("User.noScreen"), - role: eb.ref("GroupMember.role"), note: eb.ref("GroupMember.note"), weapons: matchProfileWeapons(eb), languages: eb.ref("User.languages"), @@ -122,7 +120,6 @@ export async function insert(args: CreateGroupArgs) { .values({ groupId: createdGroup.id, userId: args.userId, - role: "OWNER", }) .execute(); @@ -141,17 +138,13 @@ export async function insert(args: CreateGroupArgs) { type CreateGroupFromPreviousGroupArgs = { previousGroupId: number; - members: { - id: number; - role: Tables["GroupMember"]["role"]; - }[]; + memberUserIds: number[]; status?: Exclude; }; export async function insertFromPrevious( args: CreateGroupFromPreviousGroupArgs, ) { const status = args.status ?? "PREPARING"; - const membersWithEnsuredOwner = ensureOwnerRole(args.members); return db.transaction().execute(async (trx) => { const createdGroup = await trx @@ -175,10 +168,9 @@ export async function insertFromPrevious( await trx .insertInto("GroupMember") .values( - membersWithEnsuredOwner.map((member) => ({ + args.memberUserIds.map((userId) => ({ groupId: createdGroup.id, - userId: member.id, - role: member.role, + userId, })), ) .execute(); @@ -193,19 +185,6 @@ export async function insertFromPrevious( }); } -function ensureOwnerRole( - members: CreateGroupFromPreviousGroupArgs["members"], -): CreateGroupFromPreviousGroupArgs["members"] { - if (members.some((m) => m.role === "OWNER")) return members; - - const promoteeIndex = members.findIndex((m) => m.role === "MANAGER"); - const targetIndex = promoteeIndex !== -1 ? promoteeIndex : 0; - - return members.map((m, i) => - i === targetIndex ? { ...m, role: "OWNER" as const } : m, - ); -} - function deleteLikesByGroupId(groupId: number, trx: Transaction) { return trx .deleteFrom("GroupLike") @@ -233,34 +212,12 @@ export function morphGroups({ .where("Group.id", "=", survivingGroupId) .execute(); - const otherGroupMembers = await trx - .selectFrom("GroupMember") - .select(["GroupMember.userId", "GroupMember.role"]) + await trx + .updateTable("GroupMember") + .set({ groupId: survivingGroupId }) .where("GroupMember.groupId", "=", otherGroupId) .execute(); - for (const member of otherGroupMembers) { - const oldRole = otherGroupMembers.find( - (m) => m.userId === member.userId, - )?.role; - invariant(oldRole, "Member lacking a role"); - - await trx - .updateTable("GroupMember") - .set({ - role: - oldRole === "OWNER" - ? "MANAGER" - : oldRole === "MANAGER" - ? "MANAGER" - : "REGULAR", - groupId: survivingGroupId, - }) - .where("GroupMember.groupId", "=", otherGroupId) - .where("GroupMember.userId", "=", member.userId) - .execute(); - } - await deleteLikesByGroupId(survivingGroupId, trx); await refreshGroup(survivingGroupId, trx); @@ -312,13 +269,7 @@ async function isGroupCorrect( export async function insertMember( groupId: number, - { - userId, - role = "REGULAR", - }: { - userId: number; - role?: Tables["GroupMember"]["role"]; - }, + { userId }: { userId: number }, ) { const chatCodeToRevalidate = await db.transaction().execute(async (trx) => { await trx @@ -326,7 +277,6 @@ export async function insertMember( .values({ groupId, userId, - role, }) .execute(); @@ -656,7 +606,7 @@ export function leaveGroup(userId: number) { const userGroup = await trx .selectFrom("GroupMember") .innerJoin("Group", "Group.id", "GroupMember.groupId") - .select(["Group.id", "GroupMember.role"]) + .select(["Group.id"]) .where("userId", "=", userId) .where("Group.status", "!=", "INACTIVE") .executeTakeFirstOrThrow(); @@ -667,30 +617,17 @@ export function leaveGroup(userId: number) { .where("GroupMember.groupId", "=", userGroup.id) .execute(); - const remainingMembers = await trx + const remainingMember = await trx .selectFrom("GroupMember") - .select(["userId", "role"]) + .select(["userId"]) .where("groupId", "=", userGroup.id) - .execute(); + .executeTakeFirst(); - if (remainingMembers.length === 0) { + if (!remainingMember) { await trx.deleteFrom("Group").where("id", "=", userGroup.id).execute(); return; } - if (userGroup.role === "OWNER") { - const newOwner = - remainingMembers.find((m) => m.role === "MANAGER") ?? - remainingMembers[0]; - - await trx - .updateTable("GroupMember") - .set({ role: "OWNER" }) - .where("userId", "=", newOwner.userId) - .where("groupId", "=", userGroup.id) - .execute(); - } - const match = await trx .selectFrom("GroupMatch") .select(["GroupMatch.id"]) @@ -735,31 +672,6 @@ export function updateOwnMemberNote({ }); } -export function updateMemberRole({ - userId, - groupId, - role, -}: { - userId: number; - groupId: number; - role: Tables["GroupMember"]["role"]; -}) { - if (role === "OWNER") { - throw new Error("Can't set role to OWNER with this function"); - } - - return db.transaction().execute(async (trx) => { - await trx - .updateTable("GroupMember") - .set({ role }) - .where("userId", "=", userId) - .where("groupId", "=", groupId) - .execute(); - - await refreshGroup(groupId, trx); - }); -} - export function setPreparingGroupAsActive(groupId: number) { return db .updateTable("Group") diff --git a/app/features/sendouq/actions/q.looking.server.ts b/app/features/sendouq/actions/q.looking.server.ts index 154c18199..e7f2a5f85 100644 --- a/app/features/sendouq/actions/q.looking.server.ts +++ b/app/features/sendouq/actions/q.looking.server.ts @@ -11,7 +11,6 @@ import { import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server"; import { refreshStreamsCache } from "~/features/sendouq-streams/core/streams.server"; import { parseFormData } from "~/form/parse.server"; -import { errorToastIfFalsy } from "~/utils/remix.server"; import { assertUnreachable } from "~/utils/types"; import { SENDOUQ_PAGE, sendouQMatchPage } from "~/utils/urls"; import { groupAfterMorph } from "../core/groups"; @@ -64,17 +63,8 @@ export const action: ActionFunction = async ({ request }) => { }); try { - // this throws because there should normally be no way user loses ownership by the action of some other user - const validateIsGroupOwner = () => - errorToastIfFalsy(currentGroup.usersRole === "OWNER", "Not owner"); - const isGroupManager = () => - currentGroup.usersRole === "MANAGER" || - currentGroup.usersRole === "OWNER"; - switch (data._action) { case "LIKE": { - if (!isGroupManager()) return null; - await SQGroupRepository.insertLike({ likerGroupId: currentGroup.id, targetGroupId: data.targetGroupId, @@ -86,8 +76,6 @@ export const action: ActionFunction = async ({ request }) => { break; } case "RECHALLENGE": { - if (!isGroupManager()) return null; - await SQGroupRepository.rechallenge({ likerGroupId: currentGroup.id, targetGroupId: data.targetGroupId, @@ -98,8 +86,6 @@ export const action: ActionFunction = async ({ request }) => { break; } case "UNLIKE": { - if (!isGroupManager()) return null; - await SQGroupRepository.deleteLike({ likerGroupId: currentGroup.id, targetGroupId: data.targetGroupId, @@ -111,8 +97,6 @@ export const action: ActionFunction = async ({ request }) => { break; } case "GROUP_UP": { - if (!isGroupManager()) return null; - const allLikes = await SQGroupRepository.findAllLikesByGroupId( data.targetGroupId, ); @@ -161,8 +145,6 @@ export const action: ActionFunction = async ({ request }) => { break; } case "MATCH_UP": { - if (!isGroupManager()) return null; - const ownGroup = SendouQ.findOwnGroup(user.id); const theirGroup = SendouQ.findUncensoredGroupById(data.targetGroupId); if (!ownGroup || !theirGroup) return null; @@ -264,36 +246,6 @@ export const action: ActionFunction = async ({ request }) => { throw redirect(sendouQMatchPage(createdMatch.id)); } - case "GIVE_MANAGER": { - validateIsGroupOwner(); - - await SQGroupRepository.updateMemberRole({ - groupId: currentGroup.id, - userId: data.userId, - role: "MANAGER", - }); - - await refreshSendouQInstance(); - - revalidateGroupTopic(currentGroup.id); - - break; - } - case "REMOVE_MANAGER": { - validateIsGroupOwner(); - - await SQGroupRepository.updateMemberRole({ - groupId: currentGroup.id, - userId: data.userId, - role: "REGULAR", - }); - - await refreshSendouQInstance(); - - revalidateGroupTopic(currentGroup.id); - - break; - } case "LEAVE_GROUP": { await SQGroupRepository.leaveGroup(user.id); @@ -316,26 +268,6 @@ export const action: ActionFunction = async ({ request }) => { throw redirect(SENDOUQ_PAGE); } - case "KICK_FROM_GROUP": { - validateIsGroupOwner(); - errorToastIfFalsy(data.userId !== user.id, "Can't kick yourself"); - - await SQGroupRepository.leaveGroup(data.userId); - - await refreshSendouQInstance(); - - const remainingGroup = SendouQ.findUncensoredGroupById(currentGroup.id); - if (remainingGroup?.chatCode) { - setGroupChatMetadata({ - chatCode: remainingGroup.chatCode, - members: remainingGroup.members, - }); - } - - broadcastLookingUpdate(); - - break; - } case "REFRESH_GROUP": { await SQGroupRepository.refreshGroup(currentGroup.id); diff --git a/app/features/sendouq/actions/q.preparing.server.ts b/app/features/sendouq/actions/q.preparing.server.ts index 99ffb48cf..e7c16f79b 100644 --- a/app/features/sendouq/actions/q.preparing.server.ts +++ b/app/features/sendouq/actions/q.preparing.server.ts @@ -26,11 +26,6 @@ export const action = async ({ request }: ActionFunctionArgs) => { const ownGroup = SendouQ.findOwnGroup(user.id); errorToastIfFalsy(ownGroup, "No group found"); - // no perms, possibly just lost them so no more graceful degradation - if (ownGroup.usersRole === "REGULAR") { - return null; - } - const season = Seasons.current(); errorToastIfFalsy(season, "Season is not active"); @@ -67,10 +62,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { const { chatCodeToRevalidate } = await SQGroupRepository.insertMember( ownGroup.id, - { - userId: data.id, - role: "MANAGER", - }, + { userId: data.id }, ); if (chatCodeToRevalidate) { diff --git a/app/features/sendouq/actions/q.server.ts b/app/features/sendouq/actions/q.server.ts index f2e248b55..bace5fb82 100644 --- a/app/features/sendouq/actions/q.server.ts +++ b/app/features/sendouq/actions/q.server.ts @@ -94,10 +94,7 @@ export const action: ActionFunction = async ({ request, url }) => { const { chatCodeToRevalidate } = await SQGroupRepository.insertMember( groupInvitedTo.id, - { - userId: user.id, - role: "MANAGER", - }, + { userId: user.id }, ); if (chatCodeToRevalidate) { diff --git a/app/features/sendouq/components/GroupCard.browser.test.tsx b/app/features/sendouq/components/GroupCard.browser.test.tsx index 623e506a0..7ac08c41b 100644 --- a/app/features/sendouq/components/GroupCard.browser.test.tsx +++ b/app/features/sendouq/components/GroupCard.browser.test.tsx @@ -24,7 +24,6 @@ function createMember(overrides: Partial = {}): SQGroupMember { discordAvatar: null, customAvatarUrl: null, customUrl: null, - role: "OWNER", vc: "NO", languages: [], skill: "CALCULATING", @@ -52,7 +51,6 @@ function createGroup( tierRange: null, skillDifference: undefined, isReplay: false, - usersRole: null, noScreen: false, modePreferences: [], status: "ACTIVE", @@ -75,7 +73,6 @@ function createOwnGroupMember( discordAvatar: null, customAvatarUrl: null, customUrl: null, - role: "OWNER", vc: "NO", languages: [], skill: "CALCULATING", @@ -103,7 +100,6 @@ function createOwnGroup( tierRange: null, skillDifference: undefined, isReplay: false, - usersRole: "OWNER", noScreen: false, modePreferences: [], chatCode: null, @@ -159,8 +155,8 @@ describe("GroupCard", () => { group: createGroup({ members: [ createMember({ id: 1, username: "Player1" }), - createMember({ id: 2, username: "Player2", role: "MANAGER" }), - createMember({ id: 3, username: "Player3", role: "REGULAR" }), + createMember({ id: 2, username: "Player2" }), + createMember({ id: 3, username: "Player3" }), ], }), }); @@ -260,9 +256,9 @@ describe("GroupCard", () => { tier, members: [ createMember({ id: 1 }), - createMember({ id: 2, role: "REGULAR" }), - createMember({ id: 3, role: "REGULAR" }), - createMember({ id: 4, role: "REGULAR" }), + createMember({ id: 2 }), + createMember({ id: 3 }), + createMember({ id: 4 }), ], }), }); @@ -301,9 +297,9 @@ describe("GroupCard", () => { isReplay: true, members: [ createMember({ id: 1 }), - createMember({ id: 2, role: "REGULAR" }), - createMember({ id: 3, role: "REGULAR" }), - createMember({ id: 4, role: "REGULAR" }), + createMember({ id: 2 }), + createMember({ id: 3 }), + createMember({ id: 4 }), ], }), }); @@ -377,24 +373,6 @@ describe("GroupCard", () => { // Actual translated text is "Start match" await expect.element(screen.getByText("Start match")).toBeVisible(); }); - - test("hides actions when user is not owner or manager", async () => { - // ownGroup with REGULAR role shouldn't show action buttons - const ownGroup = createOwnGroup({ id: 2, usersRole: "REGULAR" }); - - const screen = await renderGroupCard({ - group: createGroup({ members: [createMember()] }), - action: "LIKE", - ownGroup, - displayOnly: false, - }); - - // Action button should not be rendered when user is not OWNER or MANAGER - const actionButton = screen.container.querySelector( - '[data-testid="group-card-action-button"]', - ); - expect(actionButton).toBeNull(); - }); }); describe("props", () => { diff --git a/app/features/sendouq/components/GroupCard.module.css b/app/features/sendouq/components/GroupCard.module.css index a4b34db06..0a1299e7d 100644 --- a/app/features/sendouq/components/GroupCard.module.css +++ b/app/features/sendouq/components/GroupCard.module.css @@ -99,20 +99,6 @@ height: 15px; } -.star { - min-width: 18px; - max-width: 18px; - color: var(--color-text-accent); -} - -.starFilled { - fill: var(--color-text-accent); -} - -.starInactive { - color: var(--color-text-high); -} - .displayTier { display: flex; gap: var(--s-1); diff --git a/app/features/sendouq/components/GroupCard.tsx b/app/features/sendouq/components/GroupCard.tsx index 946168960..78607f8ae 100644 --- a/app/features/sendouq/components/GroupCard.tsx +++ b/app/features/sendouq/components/GroupCard.tsx @@ -1,6 +1,6 @@ import clsx from "clsx"; import type { SqlBool } from "kysely"; -import { Mic, Star, Volume2, VolumeX } from "lucide-react"; +import { Mic, Volume2, VolumeX } from "lucide-react"; import * as React from "react"; import { Flipped } from "react-flip-toolkit"; import { useTranslation } from "react-i18next"; @@ -74,8 +74,6 @@ export function GroupCard({ ? resolveFutureMatchModes(ownGroup, group) : null; - const enableKicking = group.usersRole === "OWNER" && !displayOnly; - return ( ); })} @@ -199,9 +194,7 @@ export function GroupCard({ {group.skillDifference ? ( ) : null} - {action && - (ownGroup?.usersRole === "OWNER" || - ownGroup?.usersRole === "MANAGER") ? ( + {action ? ( - {showActions || displayOnly ? ( - - ) : null} {member.skill ? : null} @@ -509,83 +489,6 @@ function MemberSkillDifference({ ); } -function MemberRoleManager({ - member, - displayOnly, - enableKicking, -}: { - member: Pick; - displayOnly?: boolean; - enableKicking?: boolean; -}) { - const loggedInUser = useUser(); - const { t } = useTranslation(["q"]); - - if (displayOnly && member.role !== "OWNER") return null; - - return ( - - } - /> - } - > -
-
{t(`q:roles.${member.role}`)}
- {member.role !== "OWNER" && !displayOnly ? ( -
- {member.role === "REGULAR" ? ( - - {t("q:looking.groups.actions.giveManager")} - - ) : null} - {member.role === "MANAGER" ? ( - - {t("q:looking.groups.actions.removeManager")} - - ) : null} - {enableKicking && member.id !== loggedInUser?.id ? ( - - {t("q:looking.groups.actions.kick")} - - ) : null} -
- ) : null} -
-
- ); -} - function TierInfo({ skill }: { skill: TieredSkill | "CALCULATING" }) { const { t } = useTranslation(["q"]); diff --git a/app/features/sendouq/core/SendouQ.server.test.ts b/app/features/sendouq/core/SendouQ.server.test.ts index 6f9a6ef43..9c06f8bb5 100644 --- a/app/features/sendouq/core/SendouQ.server.test.ts +++ b/app/features/sendouq/core/SendouQ.server.test.ts @@ -138,28 +138,6 @@ describe("SendouQ", () => { expect(group).toBeUndefined(); }); - test("returns group with correct role when user is OWNER", async () => { - await createGroup([1, 2]); - await refreshSendouQInstance(); - - const group = SendouQ.findOwnGroup(users.id(1)); - - expect(group).toBeDefined(); - const member = group?.members.find((m) => m.id === users.id(1)); - expect(member?.role).toBe("OWNER"); - }); - - test("returns group with correct role when user is REGULAR member", async () => { - await createGroup([1, 2]); - await refreshSendouQInstance(); - - const group = SendouQ.findOwnGroup(users.id(2)); - - expect(group).toBeDefined(); - const member = group?.members.find((m) => m.id === users.id(2)); - expect(member?.role).toBe("REGULAR"); - }); - test("returns correct group when multiple groups exist", async () => { await createGroup([1, 2]); await createGroup([3, 4]); diff --git a/app/features/sendouq/core/SendouQ.server.ts b/app/features/sendouq/core/SendouQ.server.ts index d09fd538a..e50fb3eb2 100644 --- a/app/features/sendouq/core/SendouQ.server.ts +++ b/app/features/sendouq/core/SendouQ.server.ts @@ -1,6 +1,6 @@ import { isWithinInterval, sub } from "date-fns"; import * as R from "remeda"; -import type { DBBoolean, Tables } from "~/db/tables"; +import type { DBBoolean } from "~/db/tables"; import type { ParsedMemento } from "~/db/tables-json"; import type { AuthenticatedUser } from "~/features/auth/core/user.server"; import * as Seasons from "~/features/mmr/core/Seasons"; @@ -79,7 +79,6 @@ class SendouQClass { skillDifference: undefined as ParsedMemento["groups"][number]["skillDifference"], isReplay: false, - usersRole: null as Tables["GroupMember"]["role"] | null, members: group.members.map((member) => { const skill = calculatedUserSkills[String(member.id)]; @@ -116,20 +115,12 @@ class SendouQClass { /** * Finds the group that a user belongs to. - * @returns The user's group with their role, or undefined if not in a group + * @returns The user's group, or undefined if not in a group */ findOwnGroup(userId: number) { - const result = this.groups.find((group) => + return this.groups.find((group) => group.members.some((member) => member.id === userId), ); - if (!result) return; - - const member = result.members.find((m) => m.id === userId)!; - - return { - ...result, - usersRole: member.role, - }; } /** diff --git a/app/features/sendouq/q-action-schemas.ts b/app/features/sendouq/q-action-schemas.ts index e6d07e60f..9ae4b81f5 100644 --- a/app/features/sendouq/q-action-schemas.ts +++ b/app/features/sendouq/q-action-schemas.ts @@ -44,21 +44,9 @@ export const lookingSchema = z.union([ _action: _action("MATCH_UP"), targetGroupId: id, }), - z.object({ - _action: _action("GIVE_MANAGER"), - userId: id, - }), - z.object({ - _action: _action("REMOVE_MANAGER"), - userId: id, - }), z.object({ _action: _action("LEAVE_GROUP"), }), - z.object({ - _action: _action("KICK_FROM_GROUP"), - userId: id, - }), z.object({ _action: _action("REFRESH_GROUP"), }), diff --git a/app/features/sendouq/routes/q.preparing.tsx b/app/features/sendouq/routes/q.preparing.tsx index 3b10f74e3..b62e3902c 100644 --- a/app/features/sendouq/routes/q.preparing.tsx +++ b/app/features/sendouq/routes/q.preparing.tsx @@ -46,9 +46,7 @@ export default function QPreparingPage() {
- {data.group.members.length < FULL_GROUP_SIZE && - (data.group.usersRole === "OWNER" || - data.group.usersRole === "MANAGER") ? ( + {data.group.members.length < FULL_GROUP_SIZE ? ( m.id)} diff --git a/app/features/sendouq/routes/q.tsx b/app/features/sendouq/routes/q.tsx index 90adde25e..9ed9a23da 100644 --- a/app/features/sendouq/routes/q.tsx +++ b/app/features/sendouq/routes/q.tsx @@ -14,7 +14,6 @@ import { Image } from "~/components/Image"; import { LocaleTime } from "~/components/LocaleTime"; import { LocaleTimeRange } from "~/components/LocaleTimeRange"; import { Main } from "~/components/Main"; -import type { Tables } from "~/db/tables"; import { useUser } from "~/features/auth/core/user"; import type * as Seasons from "~/features/mmr/core/Seasons"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; @@ -228,7 +227,6 @@ function JoinTeamDialog({ close: () => void; members: { username: string; - role: Tables["GroupMember"]["role"]; }[]; }) { const { t, i18n } = useTranslation(["q"]); diff --git a/e2e/sendouq-match.spec.ts b/e2e/sendouq-match.spec.ts index d2cdb2867..7e8c0b8b5 100644 --- a/e2e/sendouq-match.spec.ts +++ b/e2e/sendouq-match.spec.ts @@ -191,7 +191,8 @@ test.describe("SendouQ match page", () => { isConcluded: true, }); - await impersonate(page, bravo[0].id); + // any member can re-queue the group, not only the member who created it + await impersonate(page, bravo[1].id); const match = new SendouQMatchPage(page); await match.goto(matchId); await match.lookAgain(); diff --git a/locales/da/q.json b/locales/da/q.json index 962dc77ca..22a67fb9c 100644 --- a/locales/da/q.json +++ b/locales/da/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "", "looking.groups.actions.giveManager": "", "looking.groups.actions.removeManager": "", - "looking.groups.actions.kick": "", "looking.groups.actions.leaveGroup": "", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/de/q.json b/locales/de/q.json index 17438e9a4..022f51560 100644 --- a/locales/de/q.json +++ b/locales/de/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "", "looking.groups.actions.giveManager": "", "looking.groups.actions.removeManager": "", - "looking.groups.actions.kick": "", "looking.groups.actions.leaveGroup": "", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/en/q.json b/locales/en/q.json index bc45664d1..deb978203 100644 --- a/locales/en/q.json +++ b/locales/en/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "Undo", "looking.groups.actions.giveManager": "Give manager", "looking.groups.actions.removeManager": "Remove manager", - "looking.groups.actions.kick": "Kick", "looking.groups.actions.leaveGroup": "Leave group", "looking.groups.actions.leaveGroup.confirm": "Leave this group?", "looking.groups.actions.stopLooking": "Stop looking", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "Vote no? You can't change your vote afterwards.", "match.rematch.declined": "You declined to continue", "match.rematch.fizzled": "Nobody wanted to continue", - "match.rematch.waitingCaptain": "Waiting for the captain to choose whether to re-queue", "match.rematch.rejoinQueue": "Rejoin queue", "match.rematch.backToQueue": "Back to queue", "match.rematch.offSeason": "Season has ended. The queue will reopen when the next season starts.", diff --git a/locales/es-ES/q.json b/locales/es-ES/q.json index 802f062b8..f42858683 100644 --- a/locales/es-ES/q.json +++ b/locales/es-ES/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "Deshacer", "looking.groups.actions.giveManager": "Hacer mánager", "looking.groups.actions.removeManager": "Quitar mánager", - "looking.groups.actions.kick": "Expulsar", "looking.groups.actions.leaveGroup": "Dejar grupo", "looking.groups.actions.leaveGroup.confirm": "¿Abandonar este grupo?", "looking.groups.actions.stopLooking": "Dejar de buscar", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "¿Votar no? No podrás cambiar tu voto después.", "match.rematch.declined": "Rechazaste continuar", "match.rematch.fizzled": "Nadie quiso continuar", - "match.rematch.waitingCaptain": "Esperando a que el capitán decida si volver a la cola", "match.rematch.rejoinQueue": "Volver a la cola", "match.rematch.backToQueue": "Volver a la cola", "match.rematch.offSeason": "La temporada ha terminado. La cola volverá a abrir cuando empiece la siguiente temporada.", diff --git a/locales/es-US/q.json b/locales/es-US/q.json index 5a766a9a6..2824508fc 100644 --- a/locales/es-US/q.json +++ b/locales/es-US/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "Deshacer", "looking.groups.actions.giveManager": "Hacer mánager", "looking.groups.actions.removeManager": "Quitar mánager", - "looking.groups.actions.kick": "Expulsar", "looking.groups.actions.leaveGroup": "Dejar grupo", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/fr-CA/q.json b/locales/fr-CA/q.json index 7fe311463..b2704de72 100644 --- a/locales/fr-CA/q.json +++ b/locales/fr-CA/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "", "looking.groups.actions.giveManager": "", "looking.groups.actions.removeManager": "", - "looking.groups.actions.kick": "", "looking.groups.actions.leaveGroup": "", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/fr-EU/q.json b/locales/fr-EU/q.json index 474a040f8..d098f5cf2 100644 --- a/locales/fr-EU/q.json +++ b/locales/fr-EU/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "Annulé", "looking.groups.actions.giveManager": "Promouvoir", "looking.groups.actions.removeManager": "Rétrograder", - "looking.groups.actions.kick": "Kick", "looking.groups.actions.leaveGroup": "Quitter le groupe", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/he/q.json b/locales/he/q.json index 0453fba56..a2e13ec44 100644 --- a/locales/he/q.json +++ b/locales/he/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "", "looking.groups.actions.giveManager": "", "looking.groups.actions.removeManager": "", - "looking.groups.actions.kick": "", "looking.groups.actions.leaveGroup": "", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/it/q.json b/locales/it/q.json index 1fa8c1076..e71058a12 100644 --- a/locales/it/q.json +++ b/locales/it/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "Annulla", "looking.groups.actions.giveManager": "Dai manager", "looking.groups.actions.removeManager": "Rimuovi manager", - "looking.groups.actions.kick": "Caccia", "looking.groups.actions.leaveGroup": "Lascia gruppo", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/ja/q.json b/locales/ja/q.json index 6035cf408..ec4cb6838 100644 --- a/locales/ja/q.json +++ b/locales/ja/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "戻す", "looking.groups.actions.giveManager": "マネージャーにあげる", "looking.groups.actions.removeManager": "マネージャーを外す", - "looking.groups.actions.kick": "キックする", "looking.groups.actions.leaveGroup": "グループを出る", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/ko/q.json b/locales/ko/q.json index 17438e9a4..022f51560 100644 --- a/locales/ko/q.json +++ b/locales/ko/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "", "looking.groups.actions.giveManager": "", "looking.groups.actions.removeManager": "", - "looking.groups.actions.kick": "", "looking.groups.actions.leaveGroup": "", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/nl/q.json b/locales/nl/q.json index 17438e9a4..022f51560 100644 --- a/locales/nl/q.json +++ b/locales/nl/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "", "looking.groups.actions.giveManager": "", "looking.groups.actions.removeManager": "", - "looking.groups.actions.kick": "", "looking.groups.actions.leaveGroup": "", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/pl/q.json b/locales/pl/q.json index 17438e9a4..022f51560 100644 --- a/locales/pl/q.json +++ b/locales/pl/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "", "looking.groups.actions.giveManager": "", "looking.groups.actions.removeManager": "", - "looking.groups.actions.kick": "", "looking.groups.actions.leaveGroup": "", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/pt-BR/q.json b/locales/pt-BR/q.json index 3845c1eaf..e4bfe9eae 100644 --- a/locales/pt-BR/q.json +++ b/locales/pt-BR/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "Desfazer", "looking.groups.actions.giveManager": "Dar gerência", "looking.groups.actions.removeManager": "Remover gerência", - "looking.groups.actions.kick": "Chutar (kick)", "looking.groups.actions.leaveGroup": "Sair do grupo", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/ru/q.json b/locales/ru/q.json index a4357e17c..9f626eb53 100644 --- a/locales/ru/q.json +++ b/locales/ru/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "Отменить", "looking.groups.actions.giveManager": "Дать роль менеджера", "looking.groups.actions.removeManager": "Удалить роль менеджера", - "looking.groups.actions.kick": "Выгнать", "looking.groups.actions.leaveGroup": "Покинуть группу", "looking.groups.actions.leaveGroup.confirm": "", "looking.groups.actions.stopLooking": "", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "", "match.rematch.declined": "", "match.rematch.fizzled": "", - "match.rematch.waitingCaptain": "", "match.rematch.rejoinQueue": "", "match.rematch.backToQueue": "", "match.rematch.offSeason": "", diff --git a/locales/zh/q.json b/locales/zh/q.json index 891c76776..e1c4c3fad 100644 --- a/locales/zh/q.json +++ b/locales/zh/q.json @@ -66,7 +66,6 @@ "looking.groups.actions.undo": "撤销", "looking.groups.actions.giveManager": "授予管理者权限", "looking.groups.actions.removeManager": "移除管理者权限", - "looking.groups.actions.kick": "踢出", "looking.groups.actions.leaveGroup": "离开小组", "looking.groups.actions.leaveGroup.confirm": "要离开此小组吗?", "looking.groups.actions.stopLooking": "停止匹配", @@ -188,7 +187,6 @@ "match.rematch.vote.noConfirm": "确认投反对票吗?投票后将无法更改。", "match.rematch.declined": "您已拒绝继续排队", "match.rematch.fizzled": "无人想要继续排队", - "match.rematch.waitingCaptain": "正在等待队长选择是否重新排队", "match.rematch.rejoinQueue": "重新加入队列", "match.rematch.backToQueue": "返回队列", "match.rematch.offSeason": "赛季已结束。队列将在下个赛季开始时重新开放。", diff --git a/migrations/20260807120658-remove-group-member-role.ts b/migrations/20260807120658-remove-group-member-role.ts new file mode 100644 index 000000000..7c1189e7a --- /dev/null +++ b/migrations/20260807120658-remove-group-member-role.ts @@ -0,0 +1,7 @@ +import type { Kysely } from "kysely"; + +export async function up(db: Kysely): Promise { + await db.transaction().execute(async (trx) => { + await trx.schema.alterTable("GroupMember").dropColumn("role").execute(); + }); +}