diff --git a/app/features/sendouq/routes/q.looking.tsx b/app/features/sendouq/routes/q.looking.tsx index 98af2042d..80d4c2044 100644 --- a/app/features/sendouq/routes/q.looking.tsx +++ b/app/features/sendouq/routes/q.looking.tsx @@ -19,6 +19,7 @@ import { useUser } from "~/features/auth/core/user"; import { useTopicRevalidation } from "~/features/chat/chat-hooks"; import { useMarkSqLikesSeen } from "~/features/global-status/global-status-likes-seen"; import type { UserCardData } from "~/features/user-card/user-card-types"; +import { privateNoteSentimentScore } from "~/features/user-card/user-card-utils"; import { useDateTimeFormat } from "~/hooks/intl/useDateTimeFormat"; import { useHydrated } from "~/hooks/useHydrated"; import { useMainContentWidth } from "~/hooks/useMainContentWidth"; @@ -478,18 +479,8 @@ function sortGroups( suggestedGroupIds: Set; }, ): T[] { - const sentimentScore = (group: T) => { - if (!group.members) return 0; - - let score = 0; - for (const member of group.members) { - const sentiment = userCards.get(member.id)?.privateNote?.sentiment; - if (sentiment === "NEGATIVE") return -1; - if (sentiment === "POSITIVE") score = 1; - } - - return score; - }; + const sentimentScore = (group: T) => + group.members ? privateNoteSentimentScore(group.members, userCards) : 0; return groups.toSorted((a, b) => { const aIsSuggested = suggestedGroupIds.has(a.id); diff --git a/app/features/tournament-lfg/routes/to.$id.looking.tsx b/app/features/tournament-lfg/routes/to.$id.looking.tsx index de0c2e260..f34b09444 100644 --- a/app/features/tournament-lfg/routes/to.$id.looking.tsx +++ b/app/features/tournament-lfg/routes/to.$id.looking.tsx @@ -26,6 +26,7 @@ import { UserCard, useUserCardData, } from "~/features/user-card/components/UserCard"; +import { privateNoteSentimentScore } from "~/features/user-card/user-card-utils"; import { SendouForm } from "~/form/SendouForm"; import { useHydrated } from "~/hooks/useHydrated"; import { useMainContentWidth } from "~/hooks/useMainContentWidth"; @@ -83,9 +84,13 @@ function GroupsView({ data.ownGroup?.members.length ?? data.ownTeam?.members.length ?? 0; const availableSlots = tournament.maxMembersPerTeam - ownMemberCount; - const compatibleGroups = data.groups.filter( - (group) => group.members.length <= availableSlots, - ); + const compatibleGroups = data.groups + .filter((group) => group.members.length <= availableSlots) + .toSorted( + (a, b) => + privateNoteSentimentScore(b.members, data.userCards) - + privateNoteSentimentScore(a.members, data.userCards), + ); const neutralGroups = compatibleGroups.filter( (group) => diff --git a/app/features/user-card/user-card-utils.test.ts b/app/features/user-card/user-card-utils.test.ts new file mode 100644 index 000000000..3c84cc18d --- /dev/null +++ b/app/features/user-card/user-card-utils.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from "vitest"; +import type { UserCardData } from "./user-card-types"; +import { privateNoteSentimentScore } from "./user-card-utils"; + +const cardWithSentiment = (sentiment: "POSITIVE" | "NEUTRAL" | "NEGATIVE") => + ({ + privateNote: { text: "", sentiment, updatedAt: 0 }, + }) as unknown as UserCardData; + +const userCards = new Map([ + [1, cardWithSentiment("POSITIVE")], + [2, cardWithSentiment("NEGATIVE")], + [3, cardWithSentiment("NEUTRAL")], +]); + +describe("privateNoteSentimentScore", () => { + test.each([ + { why: "no notes", memberIds: [4, 5], expected: 0 }, + { why: "neutral note", memberIds: [3], expected: 0 }, + { why: "positive note", memberIds: [1, 4], expected: 1 }, + { why: "negative note", memberIds: [2, 4], expected: -1 }, + { why: "negative outweighs positive", memberIds: [1, 2], expected: -1 }, + ])("$why", ({ memberIds, expected }) => { + expect( + privateNoteSentimentScore( + memberIds.map((id) => ({ id })), + userCards, + ), + ).toBe(expected); + }); +}); diff --git a/app/features/user-card/user-card-utils.ts b/app/features/user-card/user-card-utils.ts index cd7a9a365..e2626398e 100644 --- a/app/features/user-card/user-card-utils.ts +++ b/app/features/user-card/user-card-utils.ts @@ -1,4 +1,5 @@ import { USER_CARD } from "./user-card-constants"; +import type { UserCardData } from "./user-card-types"; /** * The claim must exceed the verified XP by no more than {@link USER_CARD.MAX_UNVERIFIED_XP_ABOVE_VERIFIED}. @@ -17,3 +18,21 @@ export function isValidUnverifiedXp({ unverified <= verified + USER_CARD.MAX_UNVERIFIED_XP_ABOVE_VERIFIED ); } + +/** + * Scores a group by the viewer's private notes on its members: -1 if any member has a negative note, + * 1 if any has a positive one (and none negative), otherwise 0. Sort descending to float liked groups up. + */ +export function privateNoteSentimentScore( + members: { id: number }[], + userCards: Map, +): number { + let score = 0; + for (const member of members) { + const sentiment = userCards.get(member.id)?.privateNote?.sentiment; + if (sentiment === "NEGATIVE") return -1; + if (sentiment === "POSITIVE") score = 1; + } + + return score; +} diff --git a/changelog/2026-09-23-tournament-lfg-private-note-sorting.md b/changelog/2026-09-23-tournament-lfg-private-note-sorting.md new file mode 100644 index 000000000..405b19c15 --- /dev/null +++ b/changelog/2026-09-23-tournament-lfg-private-note-sorting.md @@ -0,0 +1,5 @@ +--- +navItem: medal +type: feature +--- +Tournament LFG shows groups with players you have a positive private note on first and negative ones last, same as SendouQ