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 }}
-
-
-
-
-
-
-
- {{ notification.message }}
-
-
+
+
+
-
- {{ 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") }}
-
+
{
confirmation: undefined,
});
- const regionStats = ref<
- Partial>>>
- >({});
+ const regionStats = ref({});
const queryPlayers = async () => {
const steamIds = onlinePlayerSteamIds.value;
diff --git a/tests/utilities/matchmakingQueueCounts.spec.ts b/tests/utilities/matchmakingQueueCounts.spec.ts
new file mode 100644
index 000000000..50b979e82
--- /dev/null
+++ b/tests/utilities/matchmakingQueueCounts.spec.ts
@@ -0,0 +1,43 @@
+import { describe, expect, it } from "vitest";
+import { playersInQueue } from "~/utilities/matchmakingPartySize";
+import { e_match_types_enum } from "~/generated/zeus";
+
+const COMPETITIVE = e_match_types_enum.Competitive;
+
+describe("playersInQueue", () => {
+ it("counts players rather than lobbies", () => {
+ const stats = {
+ "us-east": {
+ [COMPETITIVE]: [
+ { lobby: 0, players: 3 },
+ { lobby: 1, players: 1 },
+ ],
+ },
+ };
+
+ expect(playersInQueue(stats, COMPETITIVE, ["us-east"])).toBe(4);
+ });
+
+ it("counts a lobby queued in several regions once", () => {
+ const stats = {
+ "us-east": { [COMPETITIVE]: [{ lobby: 0, players: 2 }] },
+ "eu-west": { [COMPETITIVE]: [{ lobby: 0, players: 2 }] },
+ };
+
+ expect(playersInQueue(stats, COMPETITIVE, ["us-east", "eu-west"])).toBe(2);
+ });
+
+ it("adds up the regions the player is actually queued for", () => {
+ const stats = {
+ "us-east": { [COMPETITIVE]: [{ lobby: 0, players: 2 }] },
+ "eu-west": { [COMPETITIVE]: [{ lobby: 1, players: 5 }] },
+ "ap-south": { [COMPETITIVE]: [{ lobby: 2, players: 4 }] },
+ };
+
+ expect(playersInQueue(stats, COMPETITIVE, ["us-east", "eu-west"])).toBe(7);
+ });
+
+ it("is zero when nothing is queued", () => {
+ expect(playersInQueue({}, COMPETITIVE, ["us-east"])).toBe(0);
+ });
+});
diff --git a/utilities/matchmakingPartySize.ts b/utilities/matchmakingPartySize.ts
index 33e930727..47d113ccb 100644
--- a/utilities/matchmakingPartySize.ts
+++ b/utilities/matchmakingPartySize.ts
@@ -29,3 +29,39 @@ export function canPartyQueue(
return partySize <= expected / 2 || partySize === expected;
}
+
+/**
+ * One lobby waiting in a region's queue, as the api broadcasts it. A lobby that
+ * queued for several regions carries the same `lobby` index in each of them.
+ */
+export interface QueuedLobbyStat {
+ lobby: number;
+ players: number;
+}
+
+export type RegionStats = Partial<
+ Record>>
+>;
+
+/**
+ * How many players are waiting for a match type across the given regions.
+ *
+ * Counts people, not parties: a queued trio is three players waiting. Lobbies
+ * are deduplicated by index, so a lobby queued in three regions is counted
+ * once rather than three times.
+ */
+export function playersInQueue(
+ regionStats: RegionStats,
+ type: e_match_types_enum,
+ regionValues: string[],
+): number {
+ const counted = new Map();
+
+ for (const regionValue of regionValues) {
+ for (const queued of regionStats[regionValue]?.[type] ?? []) {
+ counted.set(queued.lobby, queued.players);
+ }
+ }
+
+ return [...counted.values()].reduce((total, players) => total + players, 0);
+}