ghtui 0.2.0

A keyboard-driven terminal client for GitHub, with a pull request diff viewer that goes further than GitHub's
//! Notifications.

use super::*;

/// Your notifications, as GitHub's inbox lists them: what each is about,
/// why you got it and when, the unread ones marked. Words the inbox can't
/// be filtered by are said, and so is how far a filter applied after
/// fetching searched.
pub fn notifications(page: &mut Page, query: &str, inbox: Fetched<'_, Inbox>, cx: PageCtx<'_>) {
    let PageCtx { icons, keys, now } = cx;
    filter_field(page, query, keys, "filter");
    let ignored = InboxFilter::parse(query).ignored;
    if !ignored.is_empty() {
        let words = ignored.iter().map(|w| format!("“{w}”")).collect::<Vec<_>>();
        let text = format!(
            "Not filtered by {}: notifications filter by is:unread, is:read, reason: and repo:",
            words.join(", ")
        );
        page.wrapped(vec![Seg::new(text, Role::Meta)], 0, Frame::None);
    }
    page.blank();
    let Some(inbox) = inbox.show(page, "your notifications") else {
        return;
    };
    let l = &inbox.list;
    let shown = list_state(query);
    let mut title = Vec::new();
    for (state, label) in [("all", "All"), ("unread", "Unread"), ("read", "Read")] {
        let role = if state == shown {
            Role::Strong
        } else {
            Role::Meta
        };
        if !title.is_empty() {
            title.push(Seg::new("   ", Role::Body));
        }
        title.push(link_seg(page, label, Link::State(state.to_owned()), role));
    }
    let empty = if query.is_empty() {
        "All caught up: no notifications."
    } else {
        "No notifications match this filter."
    };
    let count = vec![Seg::new(count_of(l), Role::Meta)];
    list_box(page, title, count, l, empty, |page, n| {
        item(page, n.url.clone(), |page, link| {
            let (mark, role) = if n.unread {
                ("● ", Role::Strong)
            } else {
                ("  ", Role::Body)
            };
            page.box_line(
                vec![
                    Seg::new(mark, Role::Accent),
                    Seg::new(format!("{} ", subject_icon(&n.kind, icons)), Role::Meta),
                    Seg::linked(n.title.clone(), role, link),
                ],
                vec![Seg::new(time::ago_iso(&n.updated_at, now), Role::Meta)],
                0,
            );
            let meta = format!("{} · {}", n.repo, reason_label(&n.reason));
            page.box_line(vec![Seg::new(meta, Role::Meta)], Vec::new(), 4);
        });
    });
    // The filter applies after fetching, and the search stopped short.
    if inbox.cut {
        let text = format!(
            "Searched {} notifications, {} match: Load more searches further.",
            inbox.searched,
            l.items.len()
        );
        page.blank();
        page.wrapped(vec![Seg::new(text, Role::Meta)], 0, Frame::None);
    }
}

/// What a notification is about, as an icon.
pub(super) fn subject_icon(kind: &SubjectKind, icons: Icons) -> &'static str {
    let (nerd, plain) = match kind {
        SubjectKind::PullRequest => return icons.pr_state(PrState::Open),
        SubjectKind::Discussion => return icons.comment(),
        SubjectKind::Issue => ("\u{f41b}", "◉"),
        SubjectKind::Release => ("\u{f412}", "◇"),
        SubjectKind::Commit => ("\u{f417}", "◦"),
        SubjectKind::CheckSuite => ("\u{f04b}", "▷"),
        SubjectKind::Alert => ("\u{f421}", "△"),
        SubjectKind::Other(_) => ("·", "·"),
    };
    if icons.nerd_font { nerd } else { plain }
}