From 48d346eff18c7ef20e842fb6f798a35f14685a80 Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Fri, 10 Jul 2026 22:25:38 +0300
Subject: [PATCH] UserCard: when friend request pending from another make
button accept request instead of showing as pending
---
.../friends/FriendRepository.server.ts | 2 +-
.../user-card/components/UserCard.tsx | 54 +++++++++++++-----
.../routes/user-card.$id.friendship.ts | 9 ++-
app/features/user-card/user-card-types.ts | 6 +-
e2e/user-card.spec.ts | 56 ++++++++++++++++++-
5 files changed, 108 insertions(+), 19 deletions(-)
diff --git a/app/features/friends/FriendRepository.server.ts b/app/features/friends/FriendRepository.server.ts
index 1ad6425da..64a185bb7 100644
--- a/app/features/friends/FriendRepository.server.ts
+++ b/app/features/friends/FriendRepository.server.ts
@@ -187,7 +187,7 @@ export async function findFriendRequestBetween({
}) {
return db
.selectFrom("FriendRequest")
- .select("FriendRequest.id")
+ .select(["FriendRequest.id", "FriendRequest.senderId"])
.where((eb) =>
eb.or([
eb.and([
diff --git a/app/features/user-card/components/UserCard.tsx b/app/features/user-card/components/UserCard.tsx
index 91affe56d..d95d9f661 100644
--- a/app/features/user-card/components/UserCard.tsx
+++ b/app/features/user-card/components/UserCard.tsx
@@ -253,7 +253,8 @@ function CardContent({
{friendship && !friendship.isFriend ? (
) : null}
revalidating transition (not on idle) so the accepted toast still
+ // shows before the revalidated friendship data unmounts this button
React.useEffect(() => {
if (
- previousStateRef.current !== "idle" &&
- fetcher.state === "idle" &&
+ previousStateRef.current === "submitting" &&
+ fetcher.state !== "submitting" &&
fetcher.data === null
) {
toastQueue.add(
{
- message: t("user:card.friendRequestSent"),
+ message: acceptsIncomingRequest
+ ? "Friend request accepted"
+ : t("user:card.friendRequestSent"),
variant: "success",
},
{ timeout: 5000 },
);
}
previousStateRef.current = fetcher.state;
- }, [fetcher.state, fetcher.data, t]);
+ }, [fetcher.state, fetcher.data, acceptsIncomingRequest, t]);
+
+ if (acceptsIncomingRequest) {
+ return (
+ }
+ isDisabled={fetcher.state !== "idle" || fetcher.data === null}
+ aria-label="Accept friend request"
+ onPress={() =>
+ fetcher.submit(
+ {
+ _action: "ACCEPT_REQUEST",
+ friendRequestId: incomingFriendRequestId,
+ },
+ { method: "post", action: FRIENDS_PAGE },
+ )
+ }
+ />
+ );
+ }
const requestPending =
- hasPendingFriendRequest ||
- fetcher.state !== "idle" ||
- fetcher.data === null;
+ sentFriendRequest || fetcher.state !== "idle" || fetcher.data === null;
if (requestPending) {
return (
diff --git a/app/features/user-card/routes/user-card.$id.friendship.ts b/app/features/user-card/routes/user-card.$id.friendship.ts
index fa2f88556..7355f3006 100644
--- a/app/features/user-card/routes/user-card.$id.friendship.ts
+++ b/app/features/user-card/routes/user-card.$id.friendship.ts
@@ -23,7 +23,8 @@ export const loader = async ({
if (!viewer || Number.isNaN(targetUserId)) {
return {
isFriend: false,
- hasPendingFriendRequest: false,
+ sentFriendRequest: false,
+ incomingFriendRequestId: null,
mutualFriends: [],
};
}
@@ -50,7 +51,11 @@ export const loader = async ({
return {
isFriend: Boolean(friendship),
- hasPendingFriendRequest: Boolean(pendingRequest),
+ sentFriendRequest: pendingRequest?.senderId === viewer.id,
+ incomingFriendRequestId:
+ pendingRequest && pendingRequest.senderId !== viewer.id
+ ? pendingRequest.id
+ : null,
mutualFriends,
};
};
diff --git a/app/features/user-card/user-card-types.ts b/app/features/user-card/user-card-types.ts
index be89b31cc..232e8b58f 100644
--- a/app/features/user-card/user-card-types.ts
+++ b/app/features/user-card/user-card-types.ts
@@ -25,8 +25,10 @@ export interface UserCardData extends CommonUser {
*/
export interface UserCardFriendship {
isFriend: boolean;
- /** Whether a friend request between the viewer and this user is currently pending. */
- hasPendingFriendRequest: boolean;
+ /** Whether the viewer has a pending friend request sent to this user. */
+ sentFriendRequest: boolean;
+ /** Id of this user's pending friend request to the viewer, or `null` when there is none. */
+ incomingFriendRequestId: number | null;
mutualFriends: Array;
}
diff --git a/e2e/user-card.spec.ts b/e2e/user-card.spec.ts
index 69d7112c6..5cdfed59b 100644
--- a/e2e/user-card.spec.ts
+++ b/e2e/user-card.spec.ts
@@ -1,12 +1,15 @@
+import { NZAP_TEST_ID } from "~/db/seed/constants";
import { ADMIN_ID } from "~/features/admin/admin-constants";
-import { SENDOUQ_LOOKING_PAGE } from "~/utils/urls";
+import { FRIENDS_PAGE, LFG_PAGE, SENDOUQ_LOOKING_PAGE } from "~/utils/urls";
import {
expect,
impersonate,
navigate,
seed,
+ selectUser,
submit,
test,
+ waitForPOSTResponse,
} from "./helpers/playwright";
test.describe("User card", () => {
@@ -37,3 +40,54 @@ test.describe("User card", () => {
);
});
});
+
+test.describe("User card friend request", () => {
+ test("receiver sees add friend button that accepts the incoming request", async ({
+ page,
+ }) => {
+ await seed(page);
+ await impersonate(page, NZAP_TEST_ID);
+ await navigate({ page, url: FRIENDS_PAGE });
+
+ await selectUser({ page, userName: "Sendou", labelName: "User" });
+ await submit(page);
+ await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
+
+ await impersonate(page);
+ await navigate({ page, url: LFG_PAGE });
+
+ await page.getByRole("button", { name: "N-ZAP" }).first().click();
+
+ const acceptButton = page.getByLabel("Accept friend request");
+ await expect(acceptButton).toBeVisible();
+ await expect(page.getByLabel("Friend request pending")).not.toBeVisible();
+
+ await waitForPOSTResponse(page, () => acceptButton.click());
+
+ await expect(page.getByText("Friend request accepted")).toBeAttached();
+ await expect(acceptButton).not.toBeVisible();
+ await expect(page.getByLabel("Send friend request")).not.toBeVisible();
+
+ await navigate({ page, url: FRIENDS_PAGE });
+ await expect(page.getByRole("button", { name: "N-ZAP" })).toBeVisible();
+ });
+
+ test("sender still sees pending state on the receiver's card", async ({
+ page,
+ }) => {
+ await seed(page);
+ await impersonate(page);
+ await navigate({ page, url: FRIENDS_PAGE });
+
+ await selectUser({ page, userName: "N-ZAP", labelName: "User" });
+ await submit(page);
+ await expect(page.getByRole("button", { name: "Cancel" })).toBeVisible();
+
+ await navigate({ page, url: LFG_PAGE });
+ await page.getByRole("button", { name: "N-ZAP" }).first().click();
+
+ await expect(page.getByLabel("Friend request pending")).toBeVisible();
+ await expect(page.getByLabel("Friend request pending")).toBeDisabled();
+ await expect(page.getByLabel("Accept friend request")).not.toBeVisible();
+ });
+});