From 5c0c35ad8d28f032ad0b51841476be6071033a79 Mon Sep 17 00:00:00 2001 From: Luke Policinski Date: Thu, 17 Sep 2026 23:28:58 -0400 Subject: [PATCH] bug: queue player counts, friend button state, highlight self filter, name change notifications - the matchmaking queue count counted lobbies, so a queued trio showed as 1 - the profile friend badge matched any my_friends row, so a pending request already read as "Friend"; the hero now shows add / requested / accept / friend - highlights could not be filtered to your own clips - name change notifications rendered their body as plain text, showing the escaped entities, and had no link to the player - the team owner can no longer leave their own team --- components/matchmaking/Matchmaking.vue | 23 ++---- .../notification/NotificationContext.vue | 4 +- components/notification/NotificationItem.vue | 37 +++------- components/notification/NotificationStack.vue | 10 --- pages/highlights/index.vue | 1 + pages/players/[id].vue | 70 +++++++++++-------- pages/teams/[id].vue | 11 ++- stores/MatchmakingStore.ts | 5 +- .../utilities/matchmakingQueueCounts.spec.ts | 43 ++++++++++++ utilities/matchmakingPartySize.ts | 36 ++++++++++ 10 files changed, 155 insertions(+), 85 deletions(-) create mode 100644 tests/utilities/matchmakingQueueCounts.spec.ts diff --git a/components/matchmaking/Matchmaking.vue b/components/matchmaking/Matchmaking.vue index 86f5dee83..e7b48407c 100644 --- a/components/matchmaking/Matchmaking.vue +++ b/components/matchmaking/Matchmaking.vue @@ -302,7 +302,7 @@ function releaseSwapHeight(el: Element): void { {{ - distinctInQueue( + playersInQueue( matchMakingQueueDetails.type, matchMakingQueueDetails.regions, ) @@ -378,14 +378,14 @@ function releaseSwapHeight(el: Element): void { variant="secondary" class="absolute top-2 right-2 px-2 py-0.5 text-[0.65rem] tracking-[0.12em] uppercase transition-opacity duration-200" v-if=" - distinctInQueue( + playersInQueue( type.value, preferredRegions.map((region) => region.value), ) > 0 " > {{ - distinctInQueue( + playersInQueue( type.value, preferredRegions.map((region) => region.value), ) @@ -450,6 +450,7 @@ import { e_match_types_enum, e_match_status_enum } from "~/generated/zeus"; import { toast } from "@/components/ui/toast"; import { EXPECTED_PLAYERS, + playersInQueue as countPlayersInQueue, canPartyQueue, } from "~/utilities/matchmakingPartySize"; @@ -572,18 +573,8 @@ export default { canQueueType(type: e_match_types_enum): boolean { return canPartyQueue(type, this.partySize); }, - distinctInQueue(type: e_match_types_enum, regionValues: string[]): number { - const lobbyIndexes = new Set(); - for (const regionValue of regionValues) { - const indexes = this.regionStats[regionValue]?.[type]; - if (!indexes) { - continue; - } - for (const index of indexes) { - lobbyIndexes.add(index); - } - } - return lobbyIndexes.size; + playersInQueue(type: e_match_types_enum, regionValues: string[]): number { + return countPlayersInQueue(this.regionStats, type, regionValues); }, getRegionlatencyResult(region: string): | { diff --git a/components/notification/NotificationContext.vue b/components/notification/NotificationContext.vue index 6dfe9e8b4..baef16e48 100644 --- a/components/notification/NotificationContext.vue +++ b/components/notification/NotificationContext.vue @@ -14,7 +14,9 @@ import { typedGql } from "~/generated/zeus/typedDocumentNode"; const MATCH_TYPES = ["MatchStatusChange", "MatchSupport", "MatchAbandoned"]; const SERVER_TYPES = ["DedicatedServerStatus", "DedicatedServerRconStatus"]; const NODE_TYPES = ["GameNodeStatus"]; -const PLAYER_TYPES = ["PlayerSanctioned"]; +// NameChangeRequest carries the requesting player's steam id, so an admin can +// see who is asking without leaving the bell. +const PLAYER_TYPES = ["PlayerSanctioned", "NameChangeRequest"]; const SCRIM_TYPES = [ "ScrimRequestReceived", "ScrimRequestCountered", diff --git a/components/notification/NotificationItem.vue b/components/notification/NotificationItem.vue index a1e6a2c72..856d8bb32 100644 --- a/components/notification/NotificationItem.vue +++ b/components/notification/NotificationItem.vue @@ -132,32 +132,17 @@ onBeforeUnmount(() => { {{ notification.title }} - - +

+ +

-

- {{ top.message }} -

+ + + + {{ $t("matchmaking.friends.requested") }} + @@ -3257,7 +3274,6 @@ export default { }; }>, editPlayerSheet: false, - addFriendPending: false, }; }, computed: { @@ -3365,33 +3381,39 @@ export default { this.player.steam_id === this.me.steam_id ); }, - isFriend() { - if (!this.player) { - return false; + // none / outgoing / incoming / friend. A my_friends row on its own only + // means somebody asked, which is why this is not a boolean. + friendRelationship() { + if (!this.player?.steam_id) { + return "none"; } - return !!useMatchmakingStore().friends.find((friend: any) => { - return friend.steam_id == this.player.steam_id; - }); + return useFriendActions().relationship(this.player.steam_id); + }, + friendActionPending() { + return ( + !!this.player?.steam_id && + useFriendActions().isBusy(this.player.steam_id) + ); }, canAddFriend() { return !!( this.me && this.player?.steam_id && !this.isSelfProfile && - !this.isFriend + this.friendRelationship === "none" ); }, hasRightColumn() { return ( this.isSelfProfile || this.canAddFriend || - this.isFriend || + this.friendRelationship !== "none" || this.canMessage ); }, - // Deliberately not `isFriend`, which matches any my_friends row including a - // still-pending request. The server only opens a conversation between - // accepted friends, so anything looser renders a button that fails. + // Deliberately not a plain my_friends lookup, which matches a still-pending + // request too. The server only opens a conversation between accepted + // friends, so anything looser renders a button that fails. canMessage() { return ( !!this.player && useDirectMessages().canMessage(this.player.steam_id) @@ -3503,20 +3525,12 @@ export default { }); }, async addAsFriend() { - if (!this.player?.steam_id || this.addFriendPending) return; - this.addFriendPending = true; - try { - await this.$apollo.mutate({ - mutation: typedGql("mutation")({ - insert_my_friends_one: [ - { object: { steam_id: this.player.steam_id } }, - { steam_id: true }, - ], - }), - }); - } finally { - this.addFriendPending = false; - } + if (!this.player?.steam_id || this.friendActionPending) return; + await useFriendActions().addFriend(this.player.steam_id); + }, + async acceptFriendRequest() { + if (!this.player?.steam_id || this.friendActionPending) return; + await useFriendActions().acceptFriend(this.player.steam_id); }, handleImageError(event) { const img = event.target; diff --git a/pages/teams/[id].vue b/pages/teams/[id].vue index ed14dbcab..675827dad 100644 --- a/pages/teams/[id].vue +++ b/pages/teams/[id].vue @@ -180,7 +180,7 @@ const teamHeroActionsClasses = {{ $t("common.actions.delete") }} -