Contact Count Badge
The messaging sidebar’s All tab displays a badge showing the total number of contacts matching the current filter. The count comes from the existing total_count field in the contacts API response — no new endpoints were needed.
How It Works
Section titled “How It Works”A cached_all_count signal stores the total count from the API response. The badge renders only after data loads, preventing a “0” flash on initial render.
Signal Lifecycle
Section titled “Signal Lifecycle”- Component mounts →
contacts_resourcefetches contacts →cached_all_countset fromtotal_count - Tab switch →
cached_all_countretains its value → badge stays stable, no flicker - Filter change → signal resets to
0→ resource refetches → signal updates with new count - Realtime message →
refresh_keyincrements → reset → refetch → count updates
Implementation
Section titled “Implementation”Signal Declaration
Section titled “Signal Declaration”The signal lives alongside other All tab pagination state in messaging_contact_list_component.rs:
let mut cached_all_count = use_signal(|| 0u64);Population from API Response
Section titled “Population from API Response”The existing use_effect that processes contacts_resource also sets the count:
use_effect(move || { if let Some(Ok(resp)) = contacts_resource.read().as_ref() { all_contacts.set(resp.contacts.clone()); all_total_pages.set(resp.total_pages); all_current_page.set(resp.page); cached_all_count.set(resp.total_count); }});total_count is a full server-side count across all pages, not just the current page size.
Reset on Filter/Refresh Changes
Section titled “Reset on Filter/Refresh Changes”A reactive effect resets the count when filters or the refresh key change:
use_effect(use_reactive!(|filter, refresh_key| { let _ = (filter, refresh_key); all_contacts.set(Vec::new()); all_current_page.set(0); all_total_pages.set(1); cached_all_count.set(0);}));Badge Rendering
Section titled “Badge Rendering”The badge renders inside the TabsTrigger for the All tab, guarded by a data-loaded check:
TabsTrigger { value: "all", "All" if contacts_resource.read().is_some() { span { class: "ml-1 text-[10px] font-semibold text-muted-foreground", "{all_count}" } }}The contacts_resource.read().is_some() guard ensures the badge only appears after the API response arrives. Without it, the initial 0 value would briefly flash before real data loads.
Styling
Section titled “Styling”The badge uses the same classes as the Needs Reply and Follow-up tab badges:
| Class | Effect |
|---|---|
ml-1 | 0.25rem left margin from tab label |
text-[10px] | 10px font size |
font-semibold | 600 weight |
text-muted-foreground | Muted gray color |
Difference from other tabs: The All badge always displays when data is loaded. The Needs Reply and Follow-up badges only appear when their count is greater than zero.
Data Source
Section titled “Data Source”The count comes from ContactListResponse.total_count, computed server-side in get_contacts_list_api:
let total_count = schemas::contact::Entity::find() .filter(condition.clone()) .count(&db) .await .or_internal_server_error("Database query failed")?;This returns the full count of contacts matching the active filter, independent of pagination.
Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
src/mods/messaging/components/messaging_contact_list_component.rs | Signal, badge rendering, reset logic |
src/mods/contact/types/contact_list_response_type.rs | ContactListResponse with total_count: u64 |
src/mods/contact/api/get_contacts_list_api.rs | API endpoint returning total_count |