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(); + }); +});