UserCard: when friend request pending from another make button accept request instead of showing as pending
Some checks are pending
E2E Tests / e2e (push) Waiting to run
Tests and checks on push / run-checks-and-tests (push) Waiting to run
Updates translation progress / update-translation-progress-issue (push) Waiting to run

This commit is contained in:
Kalle
2026-07-10 22:25:38 +03:00
parent c4939f6e92
commit 48d346eff1
5 changed files with 108 additions and 19 deletions

View File

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

View File

@@ -253,7 +253,8 @@ function CardContent({
{friendship && !friendship.isFriend ? (
<FriendRequestButton
targetUserId={data.id}
hasPendingFriendRequest={friendship.hasPendingFriendRequest}
sentFriendRequest={friendship.sentFriendRequest}
incomingFriendRequestId={friendship.incomingFriendRequestId}
/>
) : null}
<SendouButton
@@ -369,42 +370,69 @@ function NoteView({
}
/**
* Send friend request action on the card. Submits to the `/friends` route action and shows a
* checkmark once a request is pending (server-known or just sent). Cancelling a pending request
* is done on the `/friends` page.
* Friend request action on the card, submitting to the `/friends` route action. Normally sends a
* request and shows a checkmark once one is pending (server-known or just sent); when the shown
* user has already sent the viewer a request, the same add-friend press accepts it instead.
* Cancelling a pending request is done on the `/friends` page.
*/
function FriendRequestButton({
targetUserId,
hasPendingFriendRequest,
sentFriendRequest,
incomingFriendRequestId,
}: {
targetUserId: number;
hasPendingFriendRequest: boolean;
sentFriendRequest: boolean;
incomingFriendRequestId: number | null;
}) {
const { t } = useTranslation(["user"]);
const fetcher = useFetcher();
const previousStateRef = React.useRef(fetcher.state);
const acceptsIncomingRequest = incomingFriendRequestId !== null;
// fires on the submitting -> 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 (
<SendouButton
size="miniscule"
shape="circle"
icon={<UserPlus />}
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 (

View File

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

View File

@@ -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<CommonUser>;
}

View File

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