use super::*;
use crate::tui::state::{
NotificationInboxChannelLoad, NotificationInboxItem, NotificationInboxLoad,
NotificationInboxMessage, NotificationInboxTab,
};
const NOTIFICATION_INBOX_POPUP_WIDTH: u16 = 76;
pub(in crate::tui::ui) fn render_notification_inbox_popup(
frame: &mut Frame,
area: Rect,
state: &DashboardState,
) {
let Some(tab) = state.notification_inbox_tab() else {
return;
};
let popup = notification_inbox_popup_area(area);
let inner_width = usize::from(popup.width.saturating_sub(2)).max(1);
let body_lines = usize::from(popup.height.saturating_sub(6)).max(1);
let inner = render_modal_frame(frame, popup, "Inbox");
frame.render_widget(
Paragraph::new(notification_inbox_lines(
state,
tab,
body_lines,
inner_width,
)),
inner,
);
}
pub(in crate::tui::ui) fn notification_inbox_popup_area(area: Rect) -> Rect {
let height = area.height.saturating_sub(2).clamp(10, 28);
centered_rect(area, NOTIFICATION_INBOX_POPUP_WIDTH, height)
}
fn notification_inbox_lines(
state: &DashboardState,
tab: NotificationInboxTab,
body_lines: usize,
width: usize,
) -> Vec<Line<'static>> {
let items = state.notification_inbox_items();
let selected = state.selected_notification_inbox_index().unwrap_or(0);
let mut lines = vec![
notification_inbox_tab_line(
tab,
state.notification_inbox_unread_count(),
state.notification_inbox_mention_count(),
),
Line::from(Span::styled(
"─".repeat(width.max(1)),
Style::default().fg(DIM),
)),
];
let mentions_loading = tab == NotificationInboxTab::Mentions
&& state.notification_inbox_mentions_status() == Some(NotificationInboxLoad::Loading);
let mentions_failed = tab == NotificationInboxTab::Mentions
&& state.notification_inbox_mentions_status() == Some(NotificationInboxLoad::Failed);
if mentions_loading {
lines.push(notification_inbox_notice_line("Loading mentions…"));
} else if mentions_failed {
lines.push(notification_inbox_notice_line("Failed to load mentions."));
} else if items.is_empty() {
lines.push(notification_inbox_notice_line(match tab {
NotificationInboxTab::Unreads => "You're all caught up! No unread channels.",
NotificationInboxTab::Mentions => "No new mentions.",
}));
} else {
lines.extend(notification_inbox_body_lines(
&items, selected, body_lines, width,
));
}
lines.push(Line::from(Span::styled(String::new(), Style::default())));
lines.push(notification_inbox_help_line());
lines
}
fn notification_inbox_notice_line(text: &str) -> Line<'static> {
Line::from(Span::styled(text.to_owned(), Style::default().fg(DIM)))
}
fn notification_inbox_tab_line(
tab: NotificationInboxTab,
unread_count: usize,
mention_count: usize,
) -> Line<'static> {
let tab_span = |label: &str, count: usize, active: bool| {
let text = format!(" {label} ({count}) ");
let style = if active {
Style::default().fg(ACCENT).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(DIM)
};
Span::styled(text, style)
};
Line::from(vec![
tab_span(
"Unreads",
unread_count,
tab == NotificationInboxTab::Unreads,
),
Span::styled("│", Style::default().fg(DIM)),
tab_span(
"Mentions",
mention_count,
tab == NotificationInboxTab::Mentions,
),
])
}
fn notification_inbox_body_lines(
items: &[NotificationInboxItem],
selected: usize,
body_lines: usize,
width: usize,
) -> Vec<Line<'static>> {
let mut rows: Vec<(Line<'static>, Option<usize>)> = Vec::new();
for (index, item) in items.iter().enumerate() {
let card = notification_inbox_card_lines(item, index == selected, width);
for (offset, line) in card.into_iter().enumerate() {
rows.push((line, (offset == 0).then_some(index)));
}
}
let total = rows.len();
let start = if total <= body_lines {
0
} else {
let selected_line = rows
.iter()
.position(|(_, index)| *index == Some(selected))
.unwrap_or(0);
selected_line
.saturating_sub(body_lines / 3)
.min(total - body_lines)
};
let end = total.min(start + body_lines);
rows[start..end]
.iter()
.map(|(line, _)| line.clone())
.collect()
}
fn notification_inbox_card_lines(
item: &NotificationInboxItem,
selected: bool,
width: usize,
) -> Vec<Line<'static>> {
let marker = if selected { "› " } else { " " };
let card_width = width.saturating_sub(marker.width()).max(4);
let inner_width = card_width.saturating_sub(4).max(1);
let border = notification_inbox_border_style(selected);
let mut lines = vec![
Line::from(vec![
Span::styled(marker, border),
Span::styled(
format!("╭{}╮", "─".repeat(card_width.saturating_sub(2))),
border,
),
]),
notification_inbox_inner_line(notification_inbox_header_spans(item), inner_width, selected),
];
if item.messages.is_empty() {
lines.push(notification_inbox_inner_line(
vec![Span::styled(
notification_inbox_placeholder_text(item),
Style::default().fg(DIM),
)],
inner_width,
selected,
));
} else {
for message in &item.messages {
lines.push(notification_inbox_inner_line(
notification_inbox_message_spans(message),
inner_width,
selected,
));
}
}
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
format!("╰{}╯", "─".repeat(card_width.saturating_sub(2))),
border,
),
]));
lines
}
fn notification_inbox_inner_line(
content: Vec<Span<'static>>,
inner_width: usize,
selected: bool,
) -> Line<'static> {
let body = truncate_line_to_display_width(Line::from(content), inner_width);
let border = notification_inbox_border_style(selected);
let mut spans = vec![Span::raw(" "), Span::styled("│ ", border)];
spans.extend(body.spans);
spans.push(Span::styled(" │", border));
Line::from(spans)
}
fn notification_inbox_header_spans(item: &NotificationInboxItem) -> Vec<Span<'static>> {
let (badge, title_style) = channel_unread_decoration(item.unread, Style::default(), false);
let mut spans = Vec::new();
if let Some(badge) = badge {
spans.push(badge);
}
spans.push(Span::styled(
item.title.clone(),
title_style.add_modifier(Modifier::BOLD),
));
if let Some(context) = &item.context {
spans.push(Span::styled(
format!(" {context}"),
Style::default().fg(DIM),
));
}
spans
}
fn notification_inbox_message_spans(message: &NotificationInboxMessage) -> Vec<Span<'static>> {
vec![
Span::styled(
format!("{}: ", message.author),
Style::default().fg(DIM).add_modifier(Modifier::BOLD),
),
Span::styled(message.content.clone(), Style::default().fg(DIM)),
]
}
fn notification_inbox_placeholder_text(item: &NotificationInboxItem) -> String {
if item.load == NotificationInboxChannelLoad::Loading {
return "loading…".to_owned();
}
match item.unread {
ChannelUnreadState::Mentioned(count) => {
format!("{count} new mention{}", plural_suffix(count))
}
ChannelUnreadState::Notified(count) => {
format!("{count} new message{}", plural_suffix(count))
}
ChannelUnreadState::Unread => "New messages".to_owned(),
ChannelUnreadState::Seen => "No recent messages".to_owned(),
}
}
fn notification_inbox_border_style(selected: bool) -> Style {
if selected {
Style::default().fg(ACCENT).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(DIM)
}
}
fn plural_suffix(count: u32) -> &'static str {
if count == 1 { "" } else { "s" }
}
fn notification_inbox_help_line() -> Line<'static> {
Line::from(Span::styled(
popup_shortcut_help_text(&[
("Enter", "open"),
("r", "mark read"),
("a", "mark all read"),
("←/→", "switch tab"),
("Esc", "close"),
]),
Style::default().fg(DIM),
))
}