Phone Line Filter
The phone line filter adds a dropdown to the messaging sidebar that lets you filter contacts by phone line. It’s permission-gated — only users with the FilterByPhoneLine collection permission see the filter control.
The filter works across all messaging tabs (All, Needs Reply, Follow-up) and persists via URL query parameter.
Permission
Section titled “Permission”The filter requires the Message::Collection::FilterByPhoneLine permission. Grant it through Settings → Roles → Message → Collection permissions.
Message { Collection { List, Create, ViewAttachment, UploadAttachment, FilterByPhoneLine }}The permission catalog entry in src/mods/settings/types/permission_catalog_type.rs registers it as "Filter messaging by phone line" for the role editor UI.
API Endpoint
Section titled “API Endpoint”GET /api/messages/phone-lines-for-filter returns available phone lines for the current organization.
Handler: get_messaging_phone_lines_api() in src/mods/messaging/api/get_messaging_phone_lines_api.rs
Authorization: Returns 403 if the user lacks FilterByPhoneLine permission.
Response:
[ { "id": "550e8400-e29b-41d4-a716-446655440000", "display_name": "Main Number (+17863058649)" }]The endpoint calls the shared fetch_phone_line_options() service, which queries phone_number filtered by organization_id, ordered by friendly_name.
Shared Components
Section titled “Shared Components”Three items were extracted from the call module to src/shared/ for reuse across call and messaging:
| Component | File | Purpose |
|---|---|---|
PhoneLineOption | src/shared/types/phone_line_option_type.rs | Type with id: Uuid and display_name: String |
PhoneLineSelect | src/shared/components/phone_line_select_component.rs | Dropdown component with text and icon modes |
fetch_phone_line_options() | src/shared/utils/fetch_phone_line_options.rs | DB query returning Vec<PhoneLineOption> for an org |
PhoneLineSelect Props
Section titled “PhoneLineSelect Props”#[component]pub fn PhoneLineSelect( phone_lines: Vec<PhoneLineOption>, value: Option<Uuid>, on_change: EventHandler<Option<Uuid>>, #[props(default)] trigger_class: String, #[props(default)] list_class: String, #[props(default = "bottom".to_string())] list_side: String, icon: Option<Element>, // When set, renders icon-only mode)Pass icon for a compact trigger (used in the messaging sidebar). Omit it for a full-width dropdown (used in call filters).
Contact Filter Integration
Section titled “Contact Filter Integration”Filter Types
Section titled “Filter Types”Two filter structs gained a phone_number_id field:
#[serde(default)]pub phone_number_id: Option<Uuid>,pub phone_number_id: Option<Uuid>,URL Persistence
Section titled “URL Persistence”The filter state maps to the pnid query parameter in ContactFilterQuery:
#[serde(default, skip_serializing_if = "String::is_empty")]pub pnid: String,Selecting a phone line navigates to /messaging?pnid=<uuid>. Selecting “All lines” clears the parameter. The filter restores on page reload.
Query Logic
Section titled “Query Logic”All tab — apply_contact_filters_service.rs uses a subquery to include contacts with at least one message on the selected line:
if let Some(phone_number_id) = filter.phone_number_id { let msg_subquery = schemas::message::Entity::find() .select_only() .column(schemas::message::Column::ContactId) .filter(schemas::message::Column::PhoneNumberId.eq(phone_number_id)) .filter(schemas::message::Column::OrganizationId.eq(org_id)) .into_query();
condition = condition.add( schemas::contact::Column::Id.in_subquery(msg_subquery), );}Needs Reply / Follow-up tabs — get_unanswered_contacts_service.rs appends a WHERE phone_number_id = $N clause to the raw SQL query.
Database Index
Section titled “Database Index”A composite index supports the phone line filter subquery:
-- migration/src/m20260416_120000_message_add_phone_number_id_org_index.rsCREATE INDEX idx_message_phone_number_id_org_idON message (phone_number_id, organization_id);UI Behavior
Section titled “UI Behavior”In MessagingContactList, the component checks the permission and conditionally fetches phone lines:
let can_filter_by_phone_line = session.has_permission( &Permission::Message(MessagePermission::Collection( MessageCollectionPermission::FilterByPhoneLine, )),);If permitted, a phone icon (📞) appears in ContactFilterBar. An orange dot badge indicates an active filter. The filter persists across tab switches.
Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
src/mods/messaging/api/get_messaging_phone_lines_api.rs | Permission-gated phone lines endpoint |
src/shared/components/phone_line_select_component.rs | Reusable dropdown (icon + text modes) |
src/shared/utils/fetch_phone_line_options.rs | Shared DB query for phone lines |
src/shared/types/phone_line_option_type.rs | PhoneLineOption type definition |
src/mods/contact/types/contact_filter_type.rs | phone_number_id on ContactFilter |
src/mods/contact/components/filter_bar/contact_filter_bar_component.rs | Filter bar with phone line integration |
src/mods/messaging/components/messaging_contact_list_component.rs | Permission check and phone line fetch |
src/mods/contact/services/apply_contact_filters_service.rs | Subquery filter for All tab |
src/mods/messaging/services/get_unanswered_contacts_service.rs | Raw SQL filter for Needs Reply/Follow-up |