Messaging¶
Overview¶
In-app messaging connects patients with their therapists, and therapists with each other, on the web message board and in the mobile app. Conversations are either one-on-one or group. Group conversations ("care teams") are enabled per clinic by the multiProviderGroupMessaging feature flag. Feedback cards from completed sessions post into the same threads (see patient-therapist-feedback.md).
The September 2026 overhaul settled four design rules:
- One thread per set of people. Starting a conversation with people you already have a thread with opens that thread. The app never creates a duplicate, and adding a colleague never recreates a group that already exists.
- Care teams can change. Therapists can add doctors to a conversation with a patient (including a one-on-one, which then becomes a group), remove them, or leave. Each add carries a choice of whether the new doctor sees the earlier messages.
- Full names, plain roles. Everyone appears as "First Last" with a small Doctor / Patient label where the role matters. There are no title prefixes.
- Polling, not sockets. The open thread refreshes every 5 seconds and the conversation list every 15 seconds on the web. Mobile polls the open thread every 5 seconds and the inbox every 30 seconds while it is on screen.
For the engineering reference (data model, endpoints, locking, client state) see internal/messaging-architecture.md.
Who can message whom¶
| Starting user | Can start a one-on-one with | Notes |
|---|---|---|
| Patient | A therapist they are connected to | The All My Doctors shortcut opens one thread with every connected therapist (a one-on-one when there is only one). |
| Therapist | Their patients, or every active patient in the clinic when the "All active patients" dashboard is on; other therapists in the same clinic | Conversations never cross clinics. |
Group conversations are created by therapists, hold up to 20 participants, and include at most one patient. Every therapist in a group with a patient is connected to that patient automatically.
Starting a conversation¶
- Press New Conversation.
- Direct: pick a contact. Contacts show their full name, role label and email.
- Group (therapists with the flag): optionally pick one patient, pick one or more therapists from the clinic, and optionally type a name.
- If a conversation with exactly those people already exists, it opens instead, with the toast "Opened existing conversation with these participants".
A group without a typed name is shown by the names of the other participants.
Care team changes: adding and removing doctors¶
Therapists with the flag manage participants from two places: the + button in the thread header, and the Participants panel that opens when tapping "N participants" in a group header.
Adding a doctor
- Pick one or more therapists from the clinic list (people already in the conversation are hidden).
- Choose Message history: Can see previous messages (default) or Only new messages.
- Press Add. A system line such as "Anna Petrova added Boris Ivanov" appears in the thread.
If the add started from a one-on-one with a patient, that thread becomes a group and shows "Anna Petrova turned this into a group conversation". A fresh private one-on-one with the patient can be started later. If a conversation with the resulting set of people already exists, nothing is changed: the app switches to that conversation and says so.
Removing or leaving
- Remove appears next to other doctors in the Participants panel and asks for confirmation.
- Leave conversation appears on your own row.
- The patient cannot be removed, the last doctor cannot leave while a patient is in the conversation, and one-on-one membership cannot be changed.
- A removed doctor loses access immediately: the conversation disappears from their list and, if they had it open, they see "You no longer have access to this conversation" within a few seconds.
Patients see a read-only participant list.
Reading messages¶
- The newest 50 messages load first. Load earlier messages at the top loads older pages without moving the view.
- New messages scroll into view only when you are already near the bottom or you just sent a message; if you scrolled up to read, the view stays put.
- Unread badges appear on the Messages navigation item (desktop and mobile menu) and on each conversation. Messages are marked read only while the tab is visible; a hidden tab keeps the badge until you come back.
- System lines (created, added, removed, left) are centered, translated, and do not count as unread.
- The web address carries
?conversation=<id>, so a thread can be bookmarked or shared with a colleague who is in it.
Notifications¶
Each message notifies the other participants through their notification preferences (email and push by default). On mobile, tapping the notification opens that conversation directly, including when the app was closed. Until the account has accepted the consent screen, a tap only opens the app, which shows that screen first.
Sample patient conversations¶
A therapist's sample patient (internal/sample-patient.md) comes with one seeded conversation: the therapist's notes on a session, the patient's reply, and an unread feedback card. The thread appears only in apps that know about sample patients (they ask for it with includeSample=1; older builds never list it or count it as unread). Replies are stored but never delivered, and nobody else can be added to the conversation.
Mobile¶
The mobile app has the same inbox and thread with the same rules. Since 2026-10-29 a therapist's inbox has a search field above the conversations that stays at the top while the list scrolls (as the patient list's does); patients' inboxes have none, since they hold a chat or two with the care team. Every word typed has to appear in the conversation's title or in the name of someone else in it (the therapist's own name never counts, since they are in every chat), in any order and case (ё reads as е), and when nothing matches the list says so. Adding and removing participants is web-only for now; mobile reflects membership changes within about 30 seconds and returns a removed user to the inbox with an explanation. The tab bar does not show an unread badge yet.
Feature flag¶
multiProviderGroupMessaging (per clinic, off by default) gates creating groups, adding doctors (including expanding a one-on-one), removing and leaving, and the clinic-wide therapist picker. One-on-one messaging and the patient "All My Doctors" thread work without it. See feature-flags.md.
Key Files¶
| File | Role |
|---|---|
OktaPT-FE/pages/doctor/messages.tsx, OktaPT-FE/pages/patient/messages.tsx |
Message board pages (thin wrappers around the shared page hook) |
OktaPT-FE/lib/hooks/useMessagesPage.ts |
Shared page logic: list loading and polling, deep links, toasts, create/open/send handlers |
OktaPT-FE/lib/stores/messaging.ts |
Zustand store: selection, thread and list polling, read marks, participant actions |
OktaPT-FE/components/messaging/ConversationView.tsx |
Thread header, participants panel and add-doctor popover wiring |
OktaPT-FE/components/messaging/ParticipantsPanel.tsx |
Participant list with Remove / Leave |
OktaPT-FE/components/messaging/AddParticipantPopover.tsx |
Doctor picker with the message-history choice |
OktaPT-FE/components/messaging/MessageList.tsx |
Scroll container, auto-scroll rules, "Load earlier messages" |
OktaPT-FE/lib/utils/userName.ts |
Name, initials, role label and system-line formatting |
Okta-Mobile/app/(tabs)/messages.tsx, Okta-Mobile/app/conversation/[id].tsx |
Mobile inbox and thread |
Okta-Mobile/services/notificationService.ts |
Push tap routing into the conversation |
OktaPT-API/src/routes/messages/ |
Messaging endpoints, validation and types |
OktaPT-API/src/services/messaging/conversationService.ts |
Conversation identity, membership changes, unread counts |