use egui::RichText;
use crate::fmt;
use crate::model::{PeopleTab, UiAction, ViewModel};
use crate::theme::palette;
use crate::widgets;
pub fn render(ui: &mut egui::Ui, vm: &ViewModel, actions: &mut Vec<UiAction>) {
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.heading("Contacts");
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.button(RichText::new("+ Add by HD-ID").color(palette().accent))
.clicked()
{
actions.push(UiAction::OpenAddContact);
}
});
});
ui.add_space(4.0);
let request_count = vm.contact_requests.len() + vm.pending_requests.len();
ui.horizontal(|ui| {
for tab in [
PeopleTab::Contacts,
PeopleTab::Requests,
PeopleTab::Known,
PeopleTab::Verified,
PeopleTab::Blocked,
] {
let count = match tab {
PeopleTab::Contacts => vm.contacts.len(),
PeopleTab::Requests => request_count,
PeopleTab::Known => vm.known_peers.len(),
PeopleTab::Verified => vm.verified_peers.len(),
PeopleTab::Blocked => vm.blocked.len(),
};
let label = if count > 0 {
format!("{} ({count})", tab.label())
} else {
tab.label().to_string()
};
let rich = if tab == PeopleTab::Requests && request_count > 0 {
RichText::new(label).color(palette().warn)
} else {
RichText::new(label)
};
if ui.selectable_label(vm.people_tab == tab, rich).clicked() {
actions.push(UiAction::SelectPeopleTab(tab));
}
}
});
ui.separator();
egui::ScrollArea::vertical()
.auto_shrink([false, false])
.show(ui, |ui| match vm.people_tab {
PeopleTab::Contacts => contacts(ui, vm, actions),
PeopleTab::Requests => requests(ui, vm, actions),
PeopleTab::Known => known(ui, vm, actions),
PeopleTab::Verified => verified(ui, vm, actions),
PeopleTab::Blocked => blocked(ui, vm, actions),
});
}
fn empty_hint(ui: &mut egui::Ui, text: &str) {
ui.add_space(8.0);
ui.label(RichText::new(text).color(palette().text_dim));
}
fn reachability(ui: &mut egui::Ui, lan_connected: bool, reachable: bool) {
let (glyph, label, color) = if lan_connected {
("●", "lan", palette().success)
} else if reachable {
("◈", "relay", palette().accent)
} else {
("○", "offline", palette().text_dim)
};
ui.label(RichText::new(glyph).color(color));
ui.label(RichText::new(label).small().color(color));
}
fn contacts(ui: &mut egui::Ui, vm: &ViewModel, actions: &mut Vec<UiAction>) {
if vm.contacts.is_empty() {
empty_hint(
ui,
"no contacts yet — “+ Add by HD-ID” to send a contact request, \
or accept one from the Requests tab",
);
return;
}
for c in &vm.contacts {
let name = vm.peer_label(&c.fingerprint);
ui.horizontal(|ui| {
widgets::avatar::show(ui, 30.0, &c.fingerprint, &name);
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(RichText::new(&name).strong());
if c.verified {
widgets::verified_tick(ui);
}
reachability(ui, c.lan_connected, c.reachable);
});
ui.horizontal(|ui| {
ui.label(
RichText::new(fmt::display_id(&c.fingerprint))
.monospace()
.small()
.color(palette().text_dim),
);
ui.label(
RichText::new(format!("· {}", c.source))
.small()
.color(palette().text_dim),
);
});
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Remove").clicked() {
actions.push(UiAction::RemoveContact(c.fingerprint.clone()));
}
if ui.button("Block").clicked() {
actions.push(UiAction::PersonBlock(c.fingerprint.clone()));
}
if ui.button("Rename").clicked() {
actions.push(UiAction::OpenEditAlias(c.fingerprint.clone()));
}
if ui.button("Message").clicked() {
actions.push(UiAction::PersonStartDm(c.fingerprint.clone()));
}
});
});
ui.separator();
}
}
fn requests(ui: &mut egui::Ui, vm: &ViewModel, actions: &mut Vec<UiAction>) {
if vm.contact_requests.is_empty() && vm.pending_requests.is_empty() {
empty_hint(ui, "no pending requests");
return;
}
for req in &vm.contact_requests {
let name = req
.display_name
.clone()
.unwrap_or_else(|| vm.peer_label(&req.fingerprint));
ui.horizontal(|ui| {
widgets::avatar::show(ui, 28.0, &req.fingerprint, &name);
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(RichText::new(&name).strong());
ui.label(RichText::new("via relay").small().color(palette().accent));
});
ui.label(
RichText::new(fmt::display_id(&req.fingerprint))
.monospace()
.small()
.color(palette().text_dim),
);
if let Some(note) = req.note.as_deref().filter(|n| !n.is_empty()) {
ui.label(RichText::new(format!("“{note}”")).italics().small());
}
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Decline").clicked() {
actions.push(UiAction::RejectContactRequest(req.fingerprint.clone()));
}
if ui.button("Accept").clicked() {
actions.push(UiAction::AcceptContactRequest(req.fingerprint.clone()));
}
});
});
ui.separator();
}
for req in &vm.pending_requests {
ui.horizontal(|ui| {
widgets::avatar::show(ui, 28.0, &req.fingerprint, &vm.peer_label(&req.fingerprint));
ui.vertical(|ui| {
ui.label(RichText::new(vm.peer_label(&req.fingerprint)).strong());
ui.label(
RichText::new(fmt::display_id(&req.fingerprint))
.monospace()
.small()
.color(palette().text_dim),
);
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Reject").clicked() {
actions.push(UiAction::RejectRequest(req.fingerprint.clone()));
}
if ui.button("Accept").clicked() {
actions.push(UiAction::AcceptRequest(req.fingerprint.clone()));
}
});
});
ui.separator();
}
}
fn known(ui: &mut egui::Ui, vm: &ViewModel, actions: &mut Vec<UiAction>) {
if vm.known_peers.is_empty() {
empty_hint(ui, "no known peers yet — paste an invite or start a DM");
return;
}
for p in &vm.known_peers {
let online = p.connected_peer_id.is_some();
let label = match &p.fingerprint {
Some(fp) => vm.peer_label(fp),
None => p.label.clone().unwrap_or_else(|| p.address.clone()),
};
ui.horizontal(|ui| {
widgets::status_dot(ui, online);
ui.vertical(|ui| {
ui.label(RichText::new(label).strong());
ui.label(RichText::new(&p.address).monospace().small().color(palette().text_dim));
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Forget").clicked() {
actions.push(UiAction::PersonForget(p.address.clone()));
}
if let Some(fp) = &p.fingerprint {
if ui.button("Block").clicked() {
actions.push(UiAction::PersonBlock(fp.clone()));
}
}
if ui.button("Reconnect").clicked() {
actions.push(UiAction::PersonRedial(p.address.clone()));
}
if let Some(fp) = &p.fingerprint {
if ui.button("Message").clicked() {
actions.push(UiAction::PersonStartDm(fp.clone()));
}
}
});
});
ui.separator();
}
}
fn verified(ui: &mut egui::Ui, vm: &ViewModel, actions: &mut Vec<UiAction>) {
if vm.verified_peers.is_empty() {
empty_hint(ui, "no SAS-verified peers yet");
return;
}
for fp in &vm.verified_peers {
ui.horizontal(|ui| {
widgets::avatar::show(ui, 28.0, fp, &vm.peer_label(fp));
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label(RichText::new(vm.peer_label(fp)).strong());
widgets::verified_tick(ui);
});
ui.label(RichText::new(fmt::display_id(fp)).monospace().small().color(palette().text_dim));
});
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Message").clicked() {
actions.push(UiAction::PersonStartDm(fp.clone()));
}
});
});
ui.separator();
}
}
fn blocked(ui: &mut egui::Ui, vm: &ViewModel, actions: &mut Vec<UiAction>) {
if vm.blocked.is_empty() {
empty_hint(ui, "no blocked peers");
return;
}
for fp in &vm.blocked {
ui.horizontal(|ui| {
ui.label(RichText::new(fmt::display_id(fp)).monospace().color(palette().text_dim));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui.button("Unblock").clicked() {
actions.push(UiAction::PersonUnblock(fp.clone()));
}
});
});
ui.separator();
}
}