Replay indicator + add trust

This commit is contained in:
Kalle
2023-08-16 21:38:42 +03:00
parent d473cdd6a8
commit 33256cb461
10 changed files with 159 additions and 34 deletions

View File

@@ -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 ? (
<input type="hidden" name="_action" value={_action} />
) : null}
<Button
{...rest}
disabled={rest.disabled || isSubmitting}
type="submit"
name={!isActionValue ? name() : undefined}
value={!isActionValue ? value() : undefined}
data-testid={testId ?? "submit-button"}
>
{children}
</Button>
</>
<Button
{...rest}
disabled={rest.disabled || isSubmitting}
type="submit"
name={name()}
value={value()}
data-testid={testId ?? "submit-button"}
>
{children}
</Button>
);
}

View File

@@ -115,8 +115,15 @@ export function GroupCard({
{group.tier ? (
<div className="stack xs text-lighter font-bold items-center justify-center text-xs">
<TierImage tier={group.tier} width={100} />
{group.tier.name}
{group.tier.isPlus ? "+" : ""}
<div>
{group.tier.name}
{group.tier.isPlus ? "+" : ""}{" "}
{group.isReplay ? (
<>
/ <span className="text-theme-secondary">REPLAY</span>
</>
) : null}
</div>
</div>
) : null}
{action && (ownRole === "OWNER" || ownRole === "MANAGER") ? (

View File

@@ -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

View File

@@ -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"]),

View File

@@ -6,6 +6,7 @@ export type LookingGroup = {
id: number;
mapListPreference?: Group["mapListPreference"];
tier?: TieredSkill["tier"];
isReplay?: boolean;
members?: {
id: number;
discordId: string;

View File

@@ -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<GroupMember, "groupId" | "userId">;
export function findRecentMatchPlayersByUserId(userId: number) {
return stm.all({ userId }) as Array<RecentMatchPlayer>;
}

View File

@@ -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),
};
}

View File

@@ -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,
});

View File

@@ -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 (
<Dialog
isOpen={open}
@@ -383,17 +402,29 @@ function JoinTeamDialog({
closeOnAnyClick={false}
className="text-center"
>
Join group with {joinListToNaturalString(members)}?
Join the group with{" "}
{joinListToNaturalString(members.map((m) => m.discordName))}?
<fetcher.Form
className="stack horizontal justify-center sm mt-4"
className="stack horizontal justify-center sm mt-4 flex-wrap"
method="post"
>
<SubmitButton _action="JOIN_TEAM" state={fetcher.state}>
Join
</SubmitButton>
<SubmitButton
_action="JOIN_TEAM_WITH_TRUST"
state={fetcher.state}
variant="outlined"
>
Join & trust {owner.discordName}
</SubmitButton>
<Button onClick={close} variant="destructive">
No thanks
</Button>
<FormMessage type="info">
Trusting a user allows them to add you to groups without an invite
link in the future
</FormMessage>
</fetcher.Form>
</Dialog>
);

View File

@@ -7,7 +7,7 @@ const stm = sql.prepare(/*sql */ `
) values (
@trustGiverUserId,
@trustReceiverUserId
)
) on conflict do nothing
`);
export function giveTrust({