Skip to content

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.

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.

  1. Component mountscontacts_resource fetches contacts → cached_all_count set from total_count
  2. Tab switchcached_all_count retains its value → badge stays stable, no flicker
  3. Filter change → signal resets to 0 → resource refetches → signal updates with new count
  4. Realtime messagerefresh_key increments → reset → refetch → count updates

The signal lives alongside other All tab pagination state in messaging_contact_list_component.rs:

let mut cached_all_count = use_signal(|| 0u64);

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.

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);
}));

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.

The badge uses the same classes as the Needs Reply and Follow-up tab badges:

ClassEffect
ml-10.25rem left margin from tab label
text-[10px]10px font size
font-semibold600 weight
text-muted-foregroundMuted 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.

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.

FilePurpose
src/mods/messaging/components/messaging_contact_list_component.rsSignal, badge rendering, reset logic
src/mods/contact/types/contact_list_response_type.rsContactListResponse with total_count: u64
src/mods/contact/api/get_contacts_list_api.rsAPI endpoint returning total_count