Fix global indicator showing stale likes

This commit is contained in:
Kalle
2026-10-03 18:54:09 +03:00
parent 2ab43b3fde
commit 8a9c0c27bf
6 changed files with 88 additions and 35 deletions

View File

@@ -1,4 +1,4 @@
import { addHours, addMinutes, subHours } from "date-fns";
import { addHours, addMinutes, subHours, subMinutes } from "date-fns";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { backdate } from "~/db/seed/core/backdate";
import * as SQGroupFactory from "~/db/seed/factories/SQGroupFactory";
@@ -125,6 +125,25 @@ describe("resolveGlobalStatus", () => {
});
});
test("does not count a like from a group gone stale in the looking pool", async () => {
const likerGroup = await SQGroupFactory.create({
memberUserIds: userIds([5]),
});
await SQGroupFactory.create(
{ memberUserIds: userIds([1, 2]) },
{ likedByGroupIds: [likerGroup.id] },
);
await backdate("Group", likerGroup.id, {
latestActionAt: subMinutes(new Date(), 45),
});
await refreshSendouQInstance();
expect(await resolveGlobalStatus(users.id(1))).toMatchObject({
state: "SQ_QUEUED",
count: 0,
});
});
test("resolves a group inactive for too long as expired", async () => {
const group = await SQGroupFactory.create({
memberUserIds: userIds([1, 2]),

View File

@@ -470,20 +470,23 @@ export async function insertMember(
return { chatRoomIdToRevalidate };
}
/** Count of pending likes each non-inactive group has received, keyed by group id. */
export async function findCurrentReceivedLikeCounts() {
/** Ids of the groups that have pending likes on each non-inactive group, keyed by the liked group's id. */
export async function findCurrentReceivedLikerGroupIds() {
const rows = await db
.selectFrom("GroupLike")
.innerJoin("Group", "Group.id", "GroupLike.targetGroupId")
.select((eb) => [
"GroupLike.targetGroupId",
eb.fn.countAll<number>().as("count"),
])
.select(["GroupLike.targetGroupId", "GroupLike.likerGroupId"])
.where("Group.status", "!=", "INACTIVE")
.groupBy("GroupLike.targetGroupId")
.execute();
return new Map(rows.map((row) => [row.targetGroupId, row.count]));
const likerGroupIdsByTarget = new Map<number, number[]>();
for (const row of rows) {
const likerGroupIds = likerGroupIdsByTarget.get(row.targetGroupId) ?? [];
likerGroupIds.push(row.likerGroupId);
likerGroupIdsByTarget.set(row.targetGroupId, likerGroupIds);
}
return likerGroupIdsByTarget;
}
export async function findAllLikesByGroupId(groupId: number) {

View File

@@ -61,7 +61,7 @@ class SendouQClass {
readonly #isAccurateTiers;
readonly #userSkills;
readonly #intervals;
readonly #receivedLikeCounts;
readonly #receivedLikerGroupIds;
usersInQueue;
constructor(
@@ -72,9 +72,9 @@ class SendouQClass {
userSkills: calculatedUserSkills,
isAccurateTiers,
}: Awaited<ReturnType<typeof userSkills>>,
receivedLikeCounts: Map<number, number>,
receivedLikerGroupIds: Map<number, number[]>,
) {
this.#receivedLikeCounts = receivedLikeCounts;
this.#receivedLikerGroupIds = receivedLikerGroupIds;
this.#recentMatches = recentMatches;
this.#isAccurateTiers = isAccurateTiers;
this.#userSkills = calculatedUserSkills;
@@ -131,9 +131,18 @@ class SendouQClass {
);
}
/** Pending likes/challenges the group has received, 0 when none. */
/** Pending likes/challenges the group has received from groups it can see in the looking pool, 0 when none. */
likesReceivedCount(groupId: number) {
return this.#receivedLikeCounts.get(groupId) ?? 0;
const group = this.findUncensoredGroupById(groupId);
if (!group) return 0;
const visibleGroupIds = new Set(
this.#suitableLookingGroupsFor(group).map((visible) => visible.id),
);
return (this.#receivedLikerGroupIds.get(groupId) ?? []).filter(
(likerGroupId) => visibleGroupIds.has(likerGroupId),
).length;
}
/** A group by id without censoring sensitive data. */
@@ -243,6 +252,14 @@ class SendouQClass {
const ownGroup = this.findOwnGroup(userId);
if (!ownGroup) return [];
return this.#suitableLookingGroupsFor(ownGroup)
.map(this.#getGroupReplayMapper(userId))
.sort(this.#getSkillSortComparator(ownGroup.tier))
.map(this.#getAddTierRangeMapper(ownGroup.tier))
.map((group) => this.#censorGroup(group));
}
#suitableLookingGroupsFor(ownGroup: SendouQClass["groups"][number]) {
const currentMemberCountOptions =
ownGroup.members.length === 4
? [4]
@@ -252,18 +269,13 @@ class SendouQClass {
? [1, 2]
: [1, 2, 3];
return this.groups
.filter((group) =>
this.#isSuitableLookingGroup({
group,
ownGroupId: ownGroup.id,
currentMemberCountOptions,
}),
)
.map(this.#getGroupReplayMapper(userId))
.sort(this.#getSkillSortComparator(ownGroup.tier))
.map(this.#getAddTierRangeMapper(ownGroup.tier))
.map((group) => this.#censorGroup(group));
return this.groups.filter((group) =>
this.#isSuitableLookingGroup({
group,
ownGroupId: ownGroup.id,
currentMemberCountOptions,
}),
);
}
#getGroupReplayMapper(userId: number) {
@@ -588,16 +600,15 @@ export async function refreshSendouQInstance() {
async function freshSendouQInstance() {
const season = Seasons.currentOrPrevious();
const [groups, recentMatches, skills, receivedLikeCounts] = await Promise.all(
[
const [groups, recentMatches, skills, receivedLikerGroupIds] =
await Promise.all([
SQGroupRepository.findCurrentGroups(),
SQGroupRepository.findRecentlyFinishedMatches(),
userSkills(season!.nth),
SQGroupRepository.findCurrentReceivedLikeCounts(),
],
);
SQGroupRepository.findCurrentReceivedLikerGroupIds(),
]);
return new SendouQClass(groups, recentMatches, skills, receivedLikeCounts);
return new SendouQClass(groups, recentMatches, skills, receivedLikerGroupIds);
}
/** Throws a redirect when the user loads a page other than the one their SendouQ group status puts them on. */

View File

@@ -55,7 +55,7 @@ export const loader = async ({ url }: LoaderFunctionArgs) => {
groups: groupsToShow,
ownGroup,
likes: ownGroup
? await SQGroupRepository.findAllLikesByGroupId(ownGroup.id)
? await likesWithVisibleReceived(ownGroup.id, groups)
: {
given: [],
received: [],
@@ -78,3 +78,18 @@ export const loader = async ({ url }: LoaderFunctionArgs) => {
),
};
};
/** Leaves out likes from groups the looking pool hides (e.g. gone stale), matching the header's count. */
async function likesWithVisibleReceived(
ownGroupId: number,
groups: Array<{ id: number }>,
) {
const likes = await SQGroupRepository.findAllLikesByGroupId(ownGroupId);
return {
...likes,
received: likes.received.filter((like) =>
groups.some((group) => group.id === like.groupId),
),
};
}

View File

@@ -0,0 +1,5 @@
---
navItem: sendouq
type: bug
---
SendouQ likes count in the header no longer includes likes from groups that have gone inactive and are not shown in the queue

View File

@@ -874,8 +874,8 @@ export function buildCases(fx: Fixtures): {
addStatic("SQGroupRepository.findRecentlyFinishedMatches", () =>
SQGroupRepository.findRecentlyFinishedMatches(),
);
addStatic("SQGroupRepository.findCurrentReceivedLikeCounts", () =>
SQGroupRepository.findCurrentReceivedLikeCounts(),
addStatic("SQGroupRepository.findCurrentReceivedLikerGroupIds", () =>
SQGroupRepository.findCurrentReceivedLikerGroupIds(),
);
addStatic("SplatoonRotationRepository.findAll", () =>