From da98d78489706ccd612ac8d6c991e7b8dd87f2d3 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 25 Apr 2026 19:32:03 +0200 Subject: [PATCH] feat(friends): always-visible Direct-Add row with resolved-form display The Send-Friend-Request action row in the Add Friend tab previously appeared only when the typed query contained a non-edge @, leaving no way to fire a blind request for a bare local handle. Widen the gate to allow non-empty bare handles, keep the malformed @ shapes (@, @bob, bob@) hidden. When the typed query has no @, display the resolved form @ so the user sees which instance the request will hit. Submission string is unchanged. Updates FriendsPage.test.tsx: inverts the now-stale 'does not show Direct Add row for plain usernames' test into the new positive assertion, and adds a separate test for the malformed @ shapes. --- .../src/components/chat/FriendsPage.test.tsx | 27 ++++++++++++++++++- .../web/src/components/chat/FriendsPage.tsx | 14 +++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/packages/web/src/components/chat/FriendsPage.test.tsx b/packages/web/src/components/chat/FriendsPage.test.tsx index 662e6a76..82ec41ec 100644 --- a/packages/web/src/components/chat/FriendsPage.test.tsx +++ b/packages/web/src/components/chat/FriendsPage.test.tsx @@ -254,7 +254,7 @@ describe('FriendsPage', () => { }); }); - it('does not show Direct Add row for plain usernames', async () => { + it('shows Direct Add row with resolved-form display for plain usernames', async () => { const user = userEvent.setup(); renderFriendsPage(); await user.click(screen.getByText('Add Friend')); @@ -262,6 +262,31 @@ describe('FriendsPage', () => { const input = screen.getByPlaceholderText(/Search or add by username/); await user.type(input, 'marc'); + // Direct Add row appears with the resolved form `marc@`. + // The exact host depends on jsdom (localhost:3000 by default), so match by prefix. + expect(screen.getByText(/Send friend request to/)).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`marc@${window.location.host.replace(/[.+?^${}()|[\]\\]/g, '\\$&')}`))).toBeInTheDocument(); + }); + + it('does not show Direct Add row for malformed @ shapes', async () => { + const user = userEvent.setup(); + renderFriendsPage(); + await user.click(screen.getByText('Add Friend')); + + const input = screen.getByPlaceholderText(/Search or add by username/); + + // Lone @ + await user.type(input, '@'); + expect(screen.queryByText(/Send friend request to/)).not.toBeInTheDocument(); + await user.clear(input); + + // Leading @ — only domain, no baseName + await user.type(input, '@bob'); + expect(screen.queryByText(/Send friend request to/)).not.toBeInTheDocument(); + await user.clear(input); + + // Trailing @ — only baseName, no domain + await user.type(input, 'bob@'); expect(screen.queryByText(/Send friend request to/)).not.toBeInTheDocument(); }); diff --git a/packages/web/src/components/chat/FriendsPage.tsx b/packages/web/src/components/chat/FriendsPage.tsx index 8b150e54..0007c198 100644 --- a/packages/web/src/components/chat/FriendsPage.tsx +++ b/packages/web/src/components/chat/FriendsPage.tsx @@ -462,9 +462,15 @@ function AddFriendTab({ }); }, [rawSearchResults, friends, requests, selfIds, isSearchMode]); - // Direct Add detection (synchronous, not debounced) - const atIndex = query.lastIndexOf('@'); - const showDirectAdd = atIndex > 0 && atIndex < query.length - 1; + const trimmedQuery = query.trim(); + const directAt = trimmedQuery.lastIndexOf('@'); + const showDirectAdd = trimmedQuery.length > 0 + && (directAt === -1 || (directAt > 0 && directAt < trimmedQuery.length - 1)); + // Bare handle gets the home host appended for display only — submission + // still uses the raw trimmed query. + const directAddDisplay = trimmedQuery.includes('@') + ? trimmedQuery + : `${trimmedQuery}@${window.location.host}`; // Direct Add handler const handleDirectAdd = async () => { @@ -552,7 +558,7 @@ function AddFriendTab({
- Send friend request to {query.trim()} + Send friend request to {directAddDisplay}