use crate::reply::convs::ConvRow;
use crate::ui::{Aim, Model, theme};
pub mod menu;
pub const NO_WALL: &str = "pick a workspace";
pub const NO_CONVERSATIONS: &str = "no conversations here";
pub const NOT_ANSWERED: &str = "waiting to hear about this wall";
pub fn no_channel(channel: &str) -> String {
format!(
"this seat holds no channel named {channel:?}, so nothing is asked about this wall — pick one from the channels beside it"
)
}
pub const HEADING: &str = "conversations";
pub const UNCERTAIN: &str = "?";
pub fn render(ui: &mut egui::Ui, model: &mut Model) {
let Some(aim) = model.aim.clone() else {
ui.label(NO_WALL);
return;
};
ui.label(aim.address.clone());
if !model.holds(&aim.channel) {
ui.label(no_channel(&aim.channel));
return;
}
let rows = model.rows();
if rows.is_empty() {
ui.label(if model.answered.as_ref() == Some(&aim) {
NO_CONVERSATIONS
} else {
NOT_ANSWERED
});
return;
}
let reveal = model.revealing(crate::ui::keys::Pane::Conversations);
egui::ScrollArea::vertical()
.id_salt(HEADING)
.auto_shrink(false)
.show(ui, |ui| {
for row in rows {
conversation(ui, model, &aim, &row, reveal);
}
});
}
fn conversation(ui: &mut egui::Ui, model: &mut Model, aim: &Aim, row: &ConvRow, reveal: bool) {
let selected = model.conversation.as_ref() == Some(&row.root_id);
ui.horizontal(|ui| {
ui.add_space(indent(row.depth));
subtree(ui, model, row);
let ground = ui.painter().add(egui::Shape::Noop);
let seat = ui.add(
egui::Label::new(headline(row))
.truncate()
.selectable(false)
.sense(egui::Sense::click()),
);
crate::ui::act::tag(
&seat,
&[crate::verbs::TRANSCRIPT.word, crate::verbs::FOLLOW.word],
);
if selected {
ui.painter().set(
ground,
egui::Shape::rect_filled(
seat.rect.expand2(ui.spacing().button_padding),
ui.visuals().widgets.active.rounding,
ui.visuals().selection.bg_fill,
),
);
}
if selected && reveal {
seat.scroll_to_me(None);
}
if seat.clicked() {
model.select(&row.root_id.clone());
}
menu::show(&seat, model, aim, row);
});
if let Some(failure) = &row.failure {
beneath(ui, row, failure);
}
if !row.preview.is_empty() {
beneath(ui, row, &row.preview);
}
}
fn subtree(ui: &mut egui::Ui, model: &mut Model, row: &ConvRow) {
let Some(under) = row.members.checked_sub(1).filter(|under| *under > 0) else {
return;
};
let open = model.subtree_open(&row.root_id);
let word = if open {
format!("{HIDE} {under}")
} else {
format!("{SHOW} {under}")
};
if ui.small_button(word).clicked() {
model.toggle_subtree(&row.root_id.clone());
}
}
pub const SHOW: &str = "show";
pub const HIDE: &str = "hide";
fn beneath(ui: &mut egui::Ui, row: &ConvRow, said: &str) {
ui.horizontal(|ui| {
ui.add_space(indent(row.depth) + 12.0);
ui.add(
egui::Label::new(egui::RichText::new(said).color(theme::tone_ink(&row.tone)))
.truncate(),
);
});
}
fn indent(depth: u64) -> f32 {
const STEP: f32 = 16.0;
const DEEPEST: u64 = 8;
(0..depth.min(DEEPEST)).fold(0.0, |at, _| at + STEP)
}
pub fn headline(row: &ConvRow) -> String {
let mut said = vec![format!(
"{} [{}{}] {}",
row.display,
row.state.label(),
if row.uncertain { UNCERTAIN } else { "" },
age(row.age_secs)
)];
if row.attention > 0 {
said.push(format!("{} waiting", row.attention));
}
if row.members > 1 {
said.push(format!("{} members", row.members));
}
said.join(" ")
}
pub fn age(secs: i64) -> String {
let secs = secs.max(0);
for (bound, unit, per) in [(60, 's', 1), (3600, 'm', 60), (86_400, 'h', 3600)] {
if secs < bound {
return format!("{}{unit}", secs / per);
}
}
format!("{}d", secs / 86_400)
}
#[cfg(test)]
mod tests;