mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-27 04:54:25 -05:00
Fix global indicator showing stale likes
This commit is contained in:
@@ -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]),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
5
changelog/2026-10-03-sq-likes-indicator-sync.md
Normal file
5
changelog/2026-10-03-sq-likes-indicator-sync.md
Normal 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
|
||||
@@ -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", () =>
|
||||
|
||||
Reference in New Issue
Block a user