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}