Unanswered Messages
The unanswered messages feature identifies contacts whose conversations need attention. It supports two directions: Needs Reply (contact messaged you, no response yet) and Needs Follow-Up (you messaged the contact, no response yet).
Data Types
Section titled “Data Types”UnansweredDirection
Section titled “UnansweredDirection”pub enum UnansweredDirection { NeedsReply, // Last message was inbound — you need to reply NeedsFollowUp, // Last message was outbound — contact hasn't responded}Serializes as "needs_reply" and "needs_follow_up".
UnansweredContact
Section titled “UnansweredContact”pub struct UnansweredContact { pub contact_id: Uuid, pub first_name: String, pub last_name: String, pub direction: UnansweredDirection, pub last_message_id: Uuid, pub phone_number_id: Option<Uuid>, pub phone_label: Option<String>, pub last_message_at: String, // Formatted in org timezone pub last_message_body: Option<String>, pub contact_phone: Option<String>,}UnansweredContactsFilter (server-only)
Section titled “UnansweredContactsFilter (server-only)”pub struct UnansweredContactsFilter { pub phone_number_id: Option<Uuid>, pub direction: Option<UnansweredDirection>, pub time_range: Option<TimeRange>,}UnansweredContactsResponse
Section titled “UnansweredContactsResponse”pub struct UnansweredContactsResponse { pub contacts: Vec<UnansweredContact>, pub count: u32, pub needs_reply_count: u32, // Before direction/time filtering pub needs_follow_up_count: u32, // Before direction/time filtering}The needs_reply_count and needs_follow_up_count fields reflect totals before direction and time-range filtering. The UI uses these for badge counts regardless of the active sub-filter.
API Endpoint
Section titled “API Endpoint”GET /api/messages/unansweredReturns all unanswered contacts for the caller’s organization with no query parameters — the UI applies direction and time-range filtering client-side.
Permission: Requires Contact::Collection::List (all org contacts) or Contact::Collection::ListAssigned (assigned contacts only). Returns 403 if neither is granted.
Response: UnansweredContactsResponse
Service
Section titled “Service”get_unanswered_contacts in src/mods/messaging/services/get_unanswered_contacts_service.rs.
The service uses a PostgreSQL DISTINCT ON (contact_id) query ordered by created_at DESC to get the most recent message per contact in a single pass. The last message’s direction determines NeedsReply vs NeedsFollowUp.
SELECT DISTINCT ON (contact_id) id, contact_id, phone_number_id, direction, body, created_atFROM messageWHERE organization_id = $1 AND contact_id IS NOT NULL AND direction IN ('inbound', 'outbound')ORDER BY contact_id, created_at DESCAfter the query, the service:
- Computes unfiltered
needs_reply_countandneeds_follow_up_count - Applies optional direction and time-range post-filters
- Sorts remaining entries by most recent first
- Batch-fetches contact details, primary phone numbers, and org phone labels
- Assembles the response with formatted timestamps in the org’s timezone
Member scoping: Users with Contact::Collection::List see all org contacts. Users with only ListAssigned see contacts assigned to them via the contact_user join table.
Migration
Section titled “Migration”m20260326_120000_message_add_org_contact_created_index adds a partial index for query performance:
CREATE INDEX idx_message_org_contact_createdON message (organization_id, contact_id, created_at DESC)WHERE contact_id IS NOT NULL;Run just migrate to apply.
AI Assistant Tool
Section titled “AI Assistant Tool”get_unanswered_contacts is registered as an assistant tool so users can ask “show me unanswered messages” in the chat.
Input parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
phone_number_id | Option<String> | None | Filter by org phone line UUID |
time_range | String | "all_time" | today, yesterday, last_7_days, last_30_days, last_90_days, all_time |
direction | Option<String> | None | needs_reply or needs_follow_up |
The tool response includes a deep_link per contact pointing to the messaging conversation view.
Permission: Requires Message::Collection::List.
UI Components
Section titled “UI Components”Filter chips in messaging contact list
Section titled “Filter chips in messaging contact list”MessagingContactList in src/mods/messaging/components/ adds two filter tabs:
- Needs Reply — contacts with unanswered inbound messages
- Needs Follow-Up — contacts with unanswered outbound messages
Each shows a count badge. Direction and time-range sub-filters (Today, Week, Month, All) refine the view. An empty state displays when no contacts match.
Dashboard link
Section titled “Dashboard link”The dashboard’s unanswered count stat links to /messaging?filter=unanswered. A global signal SHOW_UNANSWERED_FILTER activates the filter on mount:
pub static SHOW_UNANSWERED_FILTER: GlobalSignal<bool> = GlobalSignal::new(|| false);Shared utilities
Section titled “Shared utilities”Extracted to src/shared/utils/:
| Function | Purpose |
|---|---|
contact_display_name(first, last) | Joins name parts, trims whitespace |
contact_display_name_or_unknown(first, last) | Falls back to "Unknown Contact" |
format_response_time(secs) | Formats seconds as 5m 30s, 2h 15m, etc. |
rate_color_class(rate) | Returns Tailwind color class based on response rate threshold |
File Map
Section titled “File Map”| File | Purpose |
|---|---|
src/mods/messaging/types/unanswered_contact_type.rs | UnansweredContact, UnansweredDirection, filter, response types |
src/mods/messaging/services/get_unanswered_contacts_service.rs | DISTINCT ON query + permission scoping |
src/mods/messaging/api/get_unanswered_contacts_api.rs | GET /api/messages/unanswered server function |
src/mods/assistant/tools/messages/ai_get_unanswered_contacts_tool.rs | AI assistant tool registration |
src/mods/messaging/components/messaging_contact_list_component.rs | Filter chips + unanswered contact list UI |
src/mods/dashboard/components/messaging_response_insights_component.rs | Clickable unanswered count on dashboard |
src/shared/utils/ | contact_display_name, format_response_time, rate_color_class |
migration/src/m20260326_120000_*.rs | Partial index migration |