diff --git a/app/components/SubmitButton.tsx b/app/components/SubmitButton.tsx
index 1393571e14..106f565828 100644
--- a/app/components/SubmitButton.tsx
+++ b/app/components/SubmitButton.tsx
@@ -32,24 +32,16 @@ export function SubmitButton({
return undefined;
};
- // render action as hidden input to deal with a bug in older Safari versions
- const isActionValue = name() === "_action";
-
return (
- <>
- {isActionValue ? (
-
- ) : null}
-
- >
+
);
}
diff --git a/app/features/sendouq/components/GroupCard.tsx b/app/features/sendouq/components/GroupCard.tsx
index 978db63a3a..fa4a3ea7e1 100644
--- a/app/features/sendouq/components/GroupCard.tsx
+++ b/app/features/sendouq/components/GroupCard.tsx
@@ -115,8 +115,15 @@ export function GroupCard({
{group.tier ? (
- {group.tier.name}
- {group.tier.isPlus ? "+" : ""}
+
+ {group.tier.name}
+ {group.tier.isPlus ? "+" : ""}{" "}
+ {group.isReplay ? (
+ <>
+ / REPLAY
+ >
+ ) : null}
+
) : null}
{action && (ownRole === "OWNER" || ownRole === "MANAGER") ? (
diff --git a/app/features/sendouq/core/groups.server.ts b/app/features/sendouq/core/groups.server.ts
index 97047abb20..7ee8389a1b 100644
--- a/app/features/sendouq/core/groups.server.ts
+++ b/app/features/sendouq/core/groups.server.ts
@@ -12,6 +12,7 @@ import type {
SkillTierInterval,
TieredSkill,
} from "~/features/mmr/tiered.server";
+import type { RecentMatchPlayer } from "../queries/findRecentMatchPlayersByUserId.server";
export function divideGroups({
groups,
@@ -97,6 +98,48 @@ export function filterOutGroupsWithIncompatibleMapListPreference(
};
}
+const MIN_PLAYERS_FOR_REPLAY = 3;
+export function addReplayIndicator({
+ groups,
+ recentMatchPlayers,
+ userId,
+}: {
+ groups: DividedGroupsUncensored;
+ recentMatchPlayers: RecentMatchPlayer[];
+ userId: number;
+}): DividedGroupsUncensored {
+ if (!recentMatchPlayers.length) return groups;
+
+ const ownGroupId = recentMatchPlayers.find(
+ (u) => u.userId === userId
+ )?.groupId;
+ invariant(ownGroupId, "own group not found");
+ const otherGroupId = recentMatchPlayers.find(
+ (u) => u.groupId !== ownGroupId
+ )?.groupId;
+ invariant(otherGroupId, "other group not found");
+
+ const opponentPlayers = recentMatchPlayers
+ .filter((u) => u.groupId === otherGroupId)
+ .map((p) => p.userId);
+
+ const addReplayIndicatorIfNeeded = (group: LookingGroupWithInviteCode) => {
+ const samePlayersCount = group.members.reduce(
+ (acc, cur) => (opponentPlayers.includes(cur.id) ? acc + 1 : acc),
+ 0
+ );
+
+ return { ...group, isReplay: samePlayersCount >= MIN_PLAYERS_FOR_REPLAY };
+ };
+
+ return {
+ own: groups.own,
+ likesGiven: groups.likesGiven.map(addReplayIndicatorIfNeeded),
+ likesReceived: groups.likesReceived.map(addReplayIndicatorIfNeeded),
+ neutral: groups.neutral.map(addReplayIndicatorIfNeeded),
+ };
+}
+
const censorGroupFully = ({
inviteCode: _inviteCode,
...group
diff --git a/app/features/sendouq/q-schemas.server.ts b/app/features/sendouq/q-schemas.server.ts
index f5713c5383..e5d275e7a8 100644
--- a/app/features/sendouq/q-schemas.server.ts
+++ b/app/features/sendouq/q-schemas.server.ts
@@ -22,6 +22,9 @@ export const frontPageSchema = z.union([
z.object({
_action: z.literal("JOIN_TEAM"),
}),
+ z.object({
+ _action: z.literal("JOIN_TEAM_WITH_TRUST"),
+ }),
z.object({
_action: z.literal("SET_INITIAL_SP"),
tier: z.enum(["higher", "default", "lower"]),
diff --git a/app/features/sendouq/q-types.ts b/app/features/sendouq/q-types.ts
index e0e3a0e9f0..3a32318c29 100644
--- a/app/features/sendouq/q-types.ts
+++ b/app/features/sendouq/q-types.ts
@@ -6,6 +6,7 @@ export type LookingGroup = {
id: number;
mapListPreference?: Group["mapListPreference"];
tier?: TieredSkill["tier"];
+ isReplay?: boolean;
members?: {
id: number;
discordId: string;
diff --git a/app/features/sendouq/queries/findRecentMatchPlayersByUserId.server.ts b/app/features/sendouq/queries/findRecentMatchPlayersByUserId.server.ts
new file mode 100644
index 0000000000..0897befe2c
--- /dev/null
+++ b/app/features/sendouq/queries/findRecentMatchPlayersByUserId.server.ts
@@ -0,0 +1,32 @@
+import { sql } from "~/db/sql";
+import type { GroupMember } from "~/db/types";
+
+const stm = sql.prepare(/* sql*/ `
+ with "MostRecentGroupMatch" as (
+ select
+ "GroupMatch".*
+ from "GroupMember"
+ left join "Group" on "Group"."id" = "GroupMember"."groupId"
+ inner join "GroupMatch" on "GroupMatch"."alphaGroupId" = "Group"."id"
+ or "GroupMatch"."bravoGroupId" = "Group"."id"
+ where
+ "GroupMember"."userId" = @userId
+ order by "GroupMatch"."createdAt" desc
+ limit 1
+ )
+ select
+ "GroupMember"."groupId",
+ "GroupMember"."userId"
+ from "MostRecentGroupMatch"
+ left join "Group" on "Group"."id" = "MostRecentGroupMatch"."alphaGroupId"
+ or "Group"."id" = "MostRecentGroupMatch"."bravoGroupId"
+ left join "GroupMember" on "GroupMember"."groupId" = "Group"."id"
+ where
+ "MostRecentGroupMatch"."createdAt" > unixepoch() - 60 * 60 * 2
+`);
+
+export type RecentMatchPlayer = Pick;
+
+export function findRecentMatchPlayersByUserId(userId: number) {
+ return stm.all({ userId }) as Array;
+}
diff --git a/app/features/sendouq/queries/findTeamByInviteCode.server.ts b/app/features/sendouq/queries/findTeamByInviteCode.server.ts
index 87c1370038..27cd3b9d0a 100644
--- a/app/features/sendouq/queries/findTeamByInviteCode.server.ts
+++ b/app/features/sendouq/queries/findTeamByInviteCode.server.ts
@@ -1,13 +1,17 @@
import { sql } from "~/db/sql";
-import type { Group } from "~/db/types";
-import { parseDBArray } from "~/utils/sql";
+import type { Group, GroupMember } from "~/db/types";
+import { parseDBJsonArray } from "~/utils/sql";
const stm = sql.prepare(/* sql */ `
select
"Group"."id",
"Group"."status",
json_group_array(
- "User"."discordName"
+ json_object(
+ 'id', "User"."id",
+ 'discordName', "User"."discordName",
+ 'role', "GroupMember"."role"
+ )
) as "members"
from
"Group"
@@ -19,15 +23,17 @@ const stm = sql.prepare(/* sql */ `
group by "Group"."id"
`);
-export function findTeamByInviteCode(
- inviteCode: string
-): { id: number; status: Group["status"]; members: string[] } | null {
+export function findTeamByInviteCode(inviteCode: string): {
+ id: number;
+ status: Group["status"];
+ members: { id: number; discordName: string; role: GroupMember["role"] }[];
+} | null {
const row = stm.get({ inviteCode }) as any;
if (!row) return null;
return {
id: row.id,
status: row.status,
- members: parseDBArray(row.members),
+ members: parseDBJsonArray(row.members),
};
}
diff --git a/app/features/sendouq/routes/q.looking.tsx b/app/features/sendouq/routes/q.looking.tsx
index 35771e6b45..e58cf19f4a 100644
--- a/app/features/sendouq/routes/q.looking.tsx
+++ b/app/features/sendouq/routes/q.looking.tsx
@@ -14,7 +14,7 @@ import { Main } from "~/components/Main";
import { SubmitButton } from "~/components/SubmitButton";
import { useIsMounted } from "~/hooks/useIsMounted";
import { useTranslation } from "~/hooks/useTranslation";
-import { getUser, requireUserId } from "~/modules/auth/user.server";
+import { getUserId, requireUserId } from "~/modules/auth/user.server";
import { MapPool } from "~/modules/map-pool-serializer";
import {
parseRequestFormData,
@@ -31,6 +31,7 @@ import {
import { GroupCard } from "../components/GroupCard";
import { groupAfterMorph, hasGroupManagerPerms } from "../core/groups";
import {
+ addReplayIndicator,
addSkillsToGroups,
censorGroups,
divideGroups,
@@ -68,6 +69,7 @@ import { useWindowSize } from "~/hooks/useWindowSize";
import { Tab, Tabs } from "~/components/Tabs";
import { useAutoRefresh } from "~/hooks/useAutoRefresh";
import { groupHasMatch } from "../queries/groupHasMatch.server";
+import { findRecentMatchPlayersByUserId } from "../queries/findRecentMatchPlayersByUserId.server";
export const handle: SendouRouteHandle = {
i18n: ["q"],
@@ -285,7 +287,7 @@ export const action: ActionFunction = async ({ request }) => {
};
export const loader = async ({ request }: LoaderArgs) => {
- const user = await getUser(request);
+ const user = await getUserId(request);
const currentGroup = user ? findCurrentGroupByUserId(user.id) : undefined;
const redirectLocation = groupRedirectLocationByCurrentLocation({
@@ -323,8 +325,16 @@ export const loader = async ({ request }: LoaderArgs) => {
? filterOutGroupsWithIncompatibleMapListPreference(groupsWithSkills)
: groupsWithSkills;
+ const groupsWithReplayIndicator = groupIsFull
+ ? addReplayIndicator({
+ groups: compatibleGroups,
+ recentMatchPlayers: findRecentMatchPlayersByUserId(user!.id),
+ userId: user!.id,
+ })
+ : compatibleGroups;
+
const censoredGroups = censorGroups({
- groups: compatibleGroups,
+ groups: groupsWithReplayIndicator,
showMembers: !groupIsFull,
showInviteCode: hasGroupManagerPerms(currentGroup.role) && !groupIsFull,
});
diff --git a/app/features/sendouq/routes/q.tsx b/app/features/sendouq/routes/q.tsx
index 62640ca307..3d40187e65 100644
--- a/app/features/sendouq/routes/q.tsx
+++ b/app/features/sendouq/routes/q.tsx
@@ -68,6 +68,9 @@ import {
DEFAULT_SKILL_LOW,
DEFAULT_SKILL_MID,
} from "~/features/mmr/mmr-constants";
+import { giveTrust } from "~/features/tournament/queries/giveTrust.server";
+import type { GroupMember } from "~/db/types";
+import invariant from "tiny-invariant";
export const handle: SendouRouteHandle = {
i18n: ["q"],
@@ -120,6 +123,7 @@ export const action: ActionFunction = async ({ request }) => {
data.direct === "true" ? SENDOUQ_LOOKING_PAGE : SENDOUQ_PREPARING_PAGE
);
}
+ case "JOIN_TEAM_WITH_TRUST":
case "JOIN_TEAM": {
const code = new URL(request.url).searchParams.get(
JOIN_CODE_SEARCH_PARAM_KEY
@@ -134,6 +138,15 @@ export const action: ActionFunction = async ({ request }) => {
groupId: teamInvitedTo.id,
userId: user.id,
});
+ if (data._action === "JOIN_TEAM_WITH_TRUST") {
+ const owner = teamInvitedTo.members.find((m) => m.role === "OWNER");
+ invariant(owner, "Owner not found");
+
+ giveTrust({
+ trustGiverUserId: user.id,
+ trustReceiverUserId: owner.id,
+ });
+ }
return redirect(
teamInvitedTo.status === "PREPARING"
@@ -372,10 +385,16 @@ function JoinTeamDialog({
}: {
open: boolean;
close: () => void;
- members: string[];
+ members: {
+ discordName: string;
+ role: GroupMember["role"];
+ }[];
}) {
const fetcher = useFetcher();
+ const owner = members.find((m) => m.role === "OWNER");
+ invariant(owner, "Owner not found");
+
return (
);
diff --git a/app/features/tournament/queries/giveTrust.server.ts b/app/features/tournament/queries/giveTrust.server.ts
index 0f09ae97a6..0db185f21e 100644
--- a/app/features/tournament/queries/giveTrust.server.ts
+++ b/app/features/tournament/queries/giveTrust.server.ts
@@ -7,7 +7,7 @@ const stm = sql.prepare(/*sql */ `
) values (
@trustGiverUserId,
@trustReceiverUserId
- )
+ ) on conflict do nothing
`);
export function giveTrust({