Profile & settings
The Hub is the user's home base for their own profile, their preferences, their security settings, and account-level actions. The same surface also covers viewing other people's profiles and the public share view.
Entry points
| From |
Action |
| Nav dock |
Profile avatar / icon → /hub. |
| Friend row → tap |
Opens the friend's profile at /u/:handle (handle is username or UUID). |
| Discover marker → friend |
Profile-mini → "Open profile" → full profile. |
| Public share link |
tomoda.life/u/:handle, public profile view, no auth required (degraded). |
Authenticated /u/:handle |
Same handler behind auth, adds friend status and a Message action. |
Primary flow — view own profile
- Open the Hub. A vertical list of navigation rows: Profile, Edit profile, Appearance, Preferences, Friends, Security, Account.
- Profile. Tapping opens
/hub/profile — the user's own profile card with avatar, name, username, bio, location, age, and tabs for Moments / Events / Friends.
- Edit profile. Tapping the edit affordance opens
/hub/profile-edit — inline editable name, bio, gender, country, and avatar.
- Save. Each field saves on blur with an inline checkmark; no global save button. Avatar uploads via OS photo picker.
Primary flow — view someone else's profile
- Reach the profile. From a friend row, search result, marker, or share link.
- See the action bar. Varies by relationship:
- Stranger → "Add friend" + "Message" (message availability depends on their preferences).
- Pending sent → "Requested" (disabled until response).
- Pending received → "Accept request" + "Reject".
- Friend → "Message" + "Unfriend" (overflow).
- Self → "Edit profile".
- Inspect. Avatar, name, location, age, mutual-friends pill, recent public moments, upcoming public events.
- Friends-only content. A non-friend sees only public moments and public events. The friends-only section is omitted, not redacted.
Sub-flows — settings
| Sub-flow |
Behavior |
| Appearance |
Three theme options: System, Dark, Light. Tapping one applies the theme immediately and persists. |
| Preferences → Notifications enabled |
Master switch — currently a stored preference, not yet enforced (no notification service). |
| Preferences → Location sharing |
Toggle for is_location_shared. When off, other users cannot see this user's location. |
| Preferences → Show activity status |
Toggle for show_activity_status. When off, friends see this user as offline regardless of presence. |
| Preferences → Read receipts |
Toggle for read_receipts_enabled. Mutual — when off, neither side sees read receipts in chats. |
| Preferences → Language |
Picker between en-US, ja-JP, zh-CN, zh-TW. Switch applies immediately to the entire app. |
| Security → Change email |
Form to change email. Sends an OTP to the new address; entering the OTP confirms the change. |
| Security → Change password |
Form requiring the current password + new password. |
| Account → Logout |
Confirmation modal. Logout clears local tokens; redirects to /auth/login. |
| Account → Delete account |
Destructive confirmation. Delete soft-deactivates the user and triggers async cleanup (media purged, friends notified, sessions invalidated). The user is signed out and lands on a goodbye screen. |
States
| State |
What the user sees |
| Hub loaded |
Rows render statically. The user's avatar at the top updates from auth context. |
| Edit field saving |
The field shows a small spinner; on success a brief checkmark. |
| Field save error |
Inline red error under the field; the value reverts to the previous on next focus. |
| Avatar uploading |
Avatar greys out with a spinner. On success it pops in. |
| Theme change |
Whole app re-themes instantly — no transition flicker. |
| Language change |
Strings swap on the next render; the user does not need to restart. |
| Logout in flight |
Buttons disable; a spinner replaces "Logout". |
| Delete confirmed |
The Account screen briefly shows "Deleting..." then navigates to the goodbye screen. |
Profile of self viewed at /u/:handle |
Same profile, but the action bar shows "Edit profile" instead of "Message"/"Add friend". |
| Public share view (signed-out) |
Degraded profile — no friend actions, no mutual count, no message affordance. Sign-up CTA is the primary call to action. |
Edge cases & gotchas
- The mini card and the full profile are two different reads. The
profile-mini popover (friend markers, radar rows, mention chips, chat
headers) is a lean identity card: name, username, avatar, bio, and
whether you are already friends. The full profile at
/u/:handle is a
separate aggregated read that adds recent moments, upcoming and past
public events, mutual friends, and the exact friendship status that
drives the action bar. Opening the full profile always refetches; the
mini card never carries the heavy payload.
- Email change requires OTP. The change is not committed until the OTP from the new address is entered. If the user cancels mid-flow, their old email stays.
- Password change does not invalidate other sessions immediately. Existing sessions on other devices remain valid until they expire or refresh fails. (Product behavior — see
product/domains/.)
- Delete is async. The user is signed out and shown a goodbye screen immediately, but the actual data removal is queued: media purge, friend invalidation, and asynchronous cleanup happen behind the scenes. Friends may briefly still see the user in their list until the cache invalidates.
- Theme = System follows the OS setting and updates in real time when the OS toggles between light and dark.
- Username collision on edit. (Currently username is set at onboarding only — see Frontend → Routing. The username field is not editable from
/hub/profile-edit.)
- Avatar removal. Tapping the avatar picker offers a "Remove" affordance. Confirming sets the avatar to null; the app renders the default initials avatar.
- Friends-only moments on someone else's profile. Visible only if you're an accepted friend. Otherwise that strip is omitted — no "this is hidden" placeholder.
- Mutual-friend count. Shown on a stranger's profile when both have friends in common. Computed server-side; the user sees a flat number, no list.
- Backgrounding during a save. The save completes asynchronously; on resume the field shows the latest saved value.
- Language persistence. The language preference is stored both locally and server-side, so re-installing the app on the same account restores the user's language.
- Logout does not revoke refresh tokens immediately. The token is invalidated locally; server-side it is invalidated on next refresh. The window is short but exists. (Auth detail — see backend.)
See also