Friends
The friend graph lives behind the Hub → Friends screen. The UX covers sending and accepting requests, searching for people, removing friends, and the connection points (suggestions, profile actions, online strip).
Entry points
| From |
Action |
| Hub → Friends |
Direct route — /hub/friends. The primary surface. |
| Profile (someone else's) |
"Add friend" / "Friends" / "Pending" action button reflects current status. |
| Discover map → friend marker |
Profile-mini opens; "Add friend" if not yet connected. |
| Radar → stranger card |
"Add friend" inline action. |
| User search anywhere |
Tapping a result lands on profile; friend status is the call-to-action there. |
Primary flow — find and add
- Open Friends. The screen opens on the Friends tab — a list of accepted friends with online indicators inline. A horizontal "Online now" strip pins to the top.
- Search. The top search input filters the friend list instantly. After 2 characters, an additional backend call returns matching users not in the friend list, grouped as "Others" below the local results.
- Tap a result. Friend → opens chat or profile. Stranger → opens profile with the "Add friend" action.
- Send a request. Tapping "Add friend" sends a request. The button flips to "Requested" and a toast confirms. The target user appears under their "Pending" tab.
- Recipient accepts. When the other user opens their Pending tab and taps ✓, both sides see the friendship as accepted. The acceptor's "Online now" strip updates within ~10 seconds.
Sub-flows
| Sub-flow |
Behavior |
| Pending received |
The Friends screen has a Pending tab showing inbound requests. Each row has ✓ Accept and ✗ Reject. Accept makes the friendship; reject deletes the request. |
| Pending sent |
The Sent tab shows outgoing requests. Each row has a "Cancel" action that withdraws the request. |
| Cancel a request |
Tapping cancel removes the request from both sides. The recipient's Pending list updates on next refresh. |
| Unfriend |
From the friend's profile or the Friends list (long-press): "Unfriend" → confirmation modal. Removing is symmetric — both sides lose the friendship and the cached friend list re-fetches. |
| Block |
From a profile, overflow → "Block". (Block is a backend status today but the user-facing UX is minimal — see "Edge cases & gotchas".) |
| Suggestion carousel |
A horizontal strip on the Friends screen surfaces "People you may know" — sourced from mutual-friends and proximity signals. (Suggestion logic is heuristic; see backend.) |
| Online now strip |
A pinned horizontal row of avatars at the top of Friends showing currently-online friends. Tap an avatar to open their profile-mini. |
| Open chat from friend row |
Tapping the chat icon on a row opens or creates the 1:1 DM room. |
| Mutual-friends badge |
Profile shows a "N mutual friends" pill under the name when the viewer is not already a friend. |
States
| State |
What the user sees |
| Friends tab empty |
A muted illustration: "No friends yet — search above or invite someone." |
| Pending tab empty |
"No pending requests." |
| Sent tab empty |
"You haven't sent any requests." |
| Search loading |
A spinner inside the input; the previous results remain in place. |
| Search no results |
"No people match that search." |
| Request sent |
The action button flips from "Add friend" to "Requested" with a checkmark. |
| Acceptance succeeded |
A toast confirms. The new friend appears at the top of the Friends list and on the Online strip if they're online. |
| Request failed |
A toast with the error. The action button reverts. |
Edge cases & gotchas
- Sender and recipient see different things. The friendship row stores
user_id_1 (sender) and user_id_2 (recipient). The recipient sees the request under "Pending"; the sender sees it under "Sent". Both can cancel.
- Accept takes the other user's ID, not the request ID. This is an API quirk that the UI hides — tapping ✓ resolves the other user from the row context. The user does not see request IDs.
- Removing a friend is symmetric. There's no asymmetric "remove from my list but keep theirs". Both sides lose access immediately.
- Block is a soft surface today. The backend status
blocked exists but the user-facing block flow (where blocked users can no longer message or see profile) is not fully wired. Treat block UX as TBD. (See product/domains/.)
- Friend cache TTL. The friend list is cached server-side for 10 minutes. A just-accepted friend may not appear in another user's friend list immediately — the user is expected to refresh the screen if they're checking on a fresh acceptance.
- Online strip is presence-driven. The "Online now" filter only shows friends whose
is_location_shared setting allows it and whose presence TTL is fresh and whose show_activity_status preference is on. A friend may be online but invisible to the strip if they've disabled activity status.
- Search is paginated implicitly. Friend results return up to 10; "Others" return up to 5. There is no "load more" on search — the user is expected to refine the query.
- Profile of a stranger when not signed in. The same profile URL works without auth (degraded view: no friend actions, no mutual count, no chat affordance).
- Self-search. The user's own row is filtered out of search results.
- Backgrounded mid-accept. The accept request is fire-and-forget — backgrounding before the toast does not invalidate it.
See also