Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Improve notification items
Erik Kundt committed 2 years ago
commit 0c5a83353698ca60830f9590c194aa3df0249d6e
parent ff1c09202c82e2abd52140b8ee77d00f0fdabd19
5 files changed +119 -40
modified src/flux/ui.rs
@@ -52,14 +52,13 @@ impl<A> Frontend<A> {
        let mut terminal = setup_terminal()?;
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);
        let mut events_rx = events();
-

+
        
        let mut root = {
            let state = state_rx.recv().await.unwrap();

            W::new(&state, self.action_tx.clone())
        };

-
        // let mut last_frame: Option<CompletedFrame> = None;
        let result: anyhow::Result<Interrupted<P>> = loop {
            tokio::select! {
                // Tick to terminate the select every N milliseconds
@@ -74,7 +73,6 @@ impl<A> Frontend<A> {
                // Catch and handle interrupt signal to gracefully shutdown
                Ok(interrupted) = interrupt_rx.recv() => {
                    let size = terminal.get_frame().size();
-
                    // terminal.set_cursor(size.width, size.height + size.y)?;
                    terminal.set_cursor(size.x, size.y)?;

                    break Ok(interrupted);
@@ -102,7 +100,6 @@ fn setup_terminal() -> anyhow::Result<Terminal<Backend>> {
}

fn restore_terminal(terminal: &mut Terminal<Backend>) -> anyhow::Result<()> {
-
    // let size = terminal.get_frame().size();
    terminal.clear()?;
    Ok(())
}
modified src/flux/ui/cob.rs
@@ -1,10 +1,13 @@
-
use anyhow::anyhow;
-

+
use radicle::identity::Did;
+
use radicle::node::{Alias, NodeId};
+
use radicle::Profile;
+
use ratatui::style::Stylize;
use ratatui::widgets::Cell;

use radicle::cob::{self, ObjectId, Timestamp};
use radicle::issue::Issues;
use radicle::node::notifications::{Notification, NotificationId, NotificationKind};
+
use radicle::node::AliasStore;
use radicle::patch::Patches;
use radicle::storage::git::Repository;
use radicle::storage::{ReadRepository, RefUpdate};
@@ -13,6 +16,25 @@ use super::widget::ToRow;
use super::{format, span};

#[derive(Clone, Debug)]
+
pub struct AuthorItem {
+
    pub nid: Option<NodeId>,
+
    pub alias: Option<Alias>,
+
    pub you: bool,
+
}
+

+
impl AuthorItem {
+
    pub fn new(nid: Option<NodeId>, profile: &Profile) -> Self {
+
        let alias = match nid {
+
            Some(nid) => profile.alias(&nid),
+
            None => None,
+
        };
+
        let you = nid.map(|nid| nid == *profile.id()).unwrap_or_default();
+

+
        Self { nid, alias, you }
+
    }
+
}
+

+
#[derive(Clone, Debug)]
pub enum NotificationKindItem {
    Branch {
        name: String,
@@ -45,12 +67,22 @@ impl TryFrom<(&Repository, &Notification)> for NotificationKindItem {
                } else {
                    (None, String::new())
                };
-
                let status = match notification.update {
-
                    RefUpdate::Updated { .. } => "updated",
-
                    RefUpdate::Created { .. } => "created",
-
                    RefUpdate::Deleted { .. } => "deleted",
-
                    RefUpdate::Skipped { .. } => "skipped",
-
                };
+
                let status = match notification
+
                    .update
+
                    .new()
+
                    .map(|oid| repo.is_ancestor_of(oid, head.unwrap()))
+
                    .transpose()
+
                {
+
                    Ok(Some(true)) => "merged",
+
                    Ok(Some(false)) | Ok(None) => match notification.update {
+
                        RefUpdate::Updated { .. } => "updated",
+
                        RefUpdate::Created { .. } => "created",
+
                        RefUpdate::Deleted { .. } => "deleted",
+
                        RefUpdate::Skipped { .. } => "skipped",
+
                    },
+
                    Err(e) => return Err(e.into()),
+
                }
+
                .to_owned();

                Ok(NotificationKindItem::Branch {
                    name: name.to_string(),
@@ -60,20 +92,28 @@ impl TryFrom<(&Repository, &Notification)> for NotificationKindItem {
                })
            }
            NotificationKind::Cob { type_name, id } => {
-
                let (category, summary) = if *type_name == *cob::issue::TYPENAME {
-
                    let issue = issues.get(id)?.ok_or(anyhow!("missing"))?;
-
                    (String::from("issue"), issue.title().to_owned())
+
                let (category, summary, status) = if *type_name == *cob::issue::TYPENAME {
+
                    let Some(issue) = issues.get(id)? else {
+
                        // Issue could have been deleted after notification was created.
+
                        anyhow::bail!("Issue deleted after notification was created");
+
                    };
+
                    (
+
                        String::from("issue"),
+
                        issue.title().to_owned(),
+
                        issue.state().to_string(),
+
                    )
                } else if *type_name == *cob::patch::TYPENAME {
-
                    let patch = patches.get(id)?.ok_or(anyhow!("missing"))?;
-
                    (String::from("patch"), patch.title().to_owned())
+
                    let Some(patch) = patches.get(id)? else {
+
                        // Patch could have been deleted after notification was created.
+
                        anyhow::bail!("patch deleted after notification was created");
+
                    };
+
                    (
+
                        String::from("patch"),
+
                        patch.title().to_owned(),
+
                        patch.state().to_string(),
+
                    )
                } else {
-
                    (type_name.to_string(), "".to_owned())
-
                };
-
                let status = match notification.update {
-
                    RefUpdate::Updated { .. } => "updated",
-
                    RefUpdate::Created { .. } => "opened",
-
                    RefUpdate::Deleted { .. } => "deleted",
-
                    RefUpdate::Skipped { .. } => "skipped",
+
                    (type_name.to_string(), "".to_owned(), String::new())
                };

                Ok(NotificationKindItem::Cob {
@@ -95,28 +135,31 @@ pub struct NotificationItem {
    pub seen: bool,
    /// Wrapped notification kind.
    pub kind: NotificationKindItem,
+
    /// The author
+
    pub author: AuthorItem,
    /// Time the update has happened.
-
    timestamp: Timestamp,
+
    pub timestamp: Timestamp,
}

-
impl TryFrom<(&Repository, &Notification)> for NotificationItem {
+
impl TryFrom<(&Profile, &Repository, &Notification)> for NotificationItem {
    type Error = anyhow::Error;

-
    fn try_from(value: (&Repository, &Notification)) -> Result<Self, Self::Error> {
-
        let (repo, notification) = value;
+
    fn try_from(value: (&Profile, &Repository, &Notification)) -> Result<Self, Self::Error> {
+
        let (profile, repo, notification) = value;
        let kind = NotificationKindItem::try_from((repo, notification))?;

        Ok(NotificationItem {
            id: notification.id,
            seen: notification.status.is_read(),
            kind,
+
            author: AuthorItem::new(notification.remote, profile),
            timestamp: notification.timestamp.into(),
        })
    }
}

-
impl ToRow<7> for NotificationItem {
-
    fn to_row(&self) -> [Cell; 7] {
+
impl ToRow<8> for NotificationItem {
+
    fn to_row(&self) -> [Cell; 8] {
        let (type_name, summary, status, kind_id) = match &self.kind {
            NotificationKindItem::Branch {
                name,
@@ -135,25 +178,48 @@ impl ToRow<7> for NotificationItem {
            }
        };

-
        let id = span::default(format!(" {}", &self.id));
+
        let id = span::notification_id(format!(" {:-03}", &self.id));
        let seen = if self.seen {
            span::blank()
        } else {
-
            span::positive(" ● ".into())
+
            span::primary(" ● ".into())
        };
-
        let type_name = span::secondary(type_name);
-
        let summary = span::default(summary.to_string());
        let kind_id = span::primary(kind_id);
-
        let status = span::default(status.to_string());
+
        let summary = span::default(summary.to_string());
+
        let type_name = span::notification_type(type_name);
+

+
        let status = match status.as_str() {
+
            "archived" => span::default(status.to_string()).yellow(),
+
            "draft" => span::default(status.to_string()).gray().dim(),
+
            "updated" => span::primary(status.to_string()),
+
            "open" | "created" => span::positive(status.to_string()),
+
            "closed" | "merged" => span::ternary(status.to_string()),
+
            _ => span::default(status.to_string()),
+
        };
+
        let author = match &self.author.alias {
+
            Some(alias) => {
+
                if self.author.you {
+
                    span::alias(format!("{} (you)", alias))
+
                } else {
+
                    span::alias(alias.to_string())
+
                }
+
            }
+
            None => match self.author.nid {
+
                Some(nid) => span::alias(format::did(&Did::from(nid))).dim(),
+
                None => span::alias("".to_string()),
+
            },
+
        };
+

        let timestamp = span::timestamp(format::timestamp(&self.timestamp));

        [
            id.into(),
            seen.into(),
-
            type_name.into(),
-
            summary.into(),
            kind_id.into(),
+
            summary.into(),
+
            type_name.into(),
            status.into(),
+
            author.into(),
            timestamp.into(),
        ]
    }
modified src/flux/ui/span.rs
@@ -19,6 +19,10 @@ pub fn secondary(content: String) -> Text<'static> {
    default(content).style(style::magenta())
}

+
pub fn ternary(content: String) -> Text<'static> {
+
    default(content).style(style::blue())
+
}
+

pub fn positive(content: String) -> Text<'static> {
    default(content).style(style::green())
}
@@ -28,6 +32,18 @@ pub fn badge(content: String) -> Text<'static> {
    default(content.to_string()).magenta().reversed()
}

+
pub fn alias(content: String) -> Text<'static> {
+
    secondary(content)
+
}
+

pub fn timestamp(content: String) -> Text<'static> {
    default(content).style(style::gray().dim())
}
+

+
pub fn notification_id(content: String) -> Text<'static> {
+
    default(content).style(style::gray().dim())
+
}
+

+
pub fn notification_type(content: String) -> Text<'static> {
+
    default(content).style(style::gray().dim())
+
}
modified src/flux/ui/theme.rs
@@ -43,9 +43,9 @@ pub mod style {

    pub fn border(focus: bool) -> Style {
        if focus {
-
            gray()
+
            Style::default().fg(Color::Indexed(239))
        } else {
-
            gray().dim()
+
            Style::default().fg(Color::Indexed(236))
        }
    }

modified src/flux/ui/widget.rs
@@ -223,7 +223,7 @@ where
            .horizontal_margin(1)
            .split(layout[0]);

-
        let header = Row::new(props.header).style(style::reset().bold().dim());
+
        let header = Row::new(props.header).style(style::reset().bold());
        let header = ratatui::widgets::Table::default()
            .column_spacing(1)
            .header(header)