From cde549e30a8f0c3faae61346b3818ccd05dca93d Mon Sep 17 00:00:00 2001 From: Erik Kundt Date: Mon, 19 Feb 2024 17:24:15 +0100 Subject: [PATCH] lib: Support notifications in flux apps --- src/flux/ui/cob.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 11 ++-- 2 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 src/flux/ui/cob.rs diff --git a/src/flux/ui/cob.rs b/src/flux/ui/cob.rs new file mode 100644 index 00000000..d4106934 --- /dev/null +++ b/src/flux/ui/cob.rs @@ -0,0 +1,160 @@ +use anyhow::anyhow; + +use ratatui::widgets::Cell; + +use radicle::cob::{self, ObjectId, Timestamp}; +use radicle::issue::Issues; +use radicle::node::notifications::{Notification, NotificationId, NotificationKind}; +use radicle::patch::Patches; +use radicle::storage::git::Repository; +use radicle::storage::{ReadRepository, RefUpdate}; + +use super::widget::ToRow; +use super::{format, span}; + +#[derive(Clone, Debug)] +pub enum NotificationKindItem { + Branch { + name: String, + summary: String, + status: String, + id: Option, + }, + Cob { + type_name: String, + summary: String, + status: String, + id: Option, + }, +} + +impl TryFrom<(&Repository, &Notification)> for NotificationKindItem { + type Error = anyhow::Error; + + fn try_from(value: (&Repository, &Notification)) -> Result { + let (repo, notification) = value; + // TODO: move out of here + let issues = Issues::open(repo)?; + let patches = Patches::open(repo)?; + + match ¬ification.kind { + NotificationKind::Branch { name } => { + let (head, message) = if let Some(head) = notification.update.new() { + let message = repo.commit(head)?.summary().unwrap_or_default().to_owned(); + (Some(head), message) + } else { + (None, String::new()) + }; + let status = match notification.update { + RefUpdate::Updated { .. } => "updated", + RefUpdate::Created { .. } => "created", + RefUpdate::Deleted { .. } => "deleted", + RefUpdate::Skipped { .. } => "skipped", + }; + + Ok(NotificationKindItem::Branch { + name: name.to_string(), + summary: message, + status: status.to_string(), + id: head.map(ObjectId::from), + }) + } + 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()) + } else if *type_name == *cob::patch::TYPENAME { + let patch = patches.get(id)?.ok_or(anyhow!("missing"))?; + (String::from("patch"), patch.title().to_owned()) + } else { + (type_name.to_string(), "".to_owned()) + }; + let status = match notification.update { + RefUpdate::Updated { .. } => "updated", + RefUpdate::Created { .. } => "opened", + RefUpdate::Deleted { .. } => "deleted", + RefUpdate::Skipped { .. } => "skipped", + }; + + Ok(NotificationKindItem::Cob { + type_name: category.to_string(), + summary: summary.to_string(), + status: status.to_string(), + id: Some(*id), + }) + } + } + } +} + +#[derive(Clone, Debug)] +pub struct NotificationItem { + /// Unique notification ID. + pub id: NotificationId, + /// Mark this notification as seen. + pub seen: bool, + /// Wrapped notification kind. + pub kind: NotificationKindItem, + /// Time the update has happened. + timestamp: Timestamp, +} + +impl TryFrom<(&Repository, &Notification)> for NotificationItem { + type Error = anyhow::Error; + + fn try_from(value: (&Repository, &Notification)) -> Result { + let (repo, notification) = value; + let kind = NotificationKindItem::try_from((repo, notification))?; + + Ok(NotificationItem { + id: notification.id, + seen: notification.status.is_read(), + kind, + timestamp: notification.timestamp.into(), + }) + } +} + +impl ToRow<7> for NotificationItem { + fn to_row(&self) -> [Cell; 7] { + let (type_name, summary, status, kind_id) = match &self.kind { + NotificationKindItem::Branch { + name, + summary, + status, + id: _, + } => ("branch".to_string(), summary, status, name.to_string()), + NotificationKindItem::Cob { + type_name, + summary, + status, + id, + } => { + let id = id.map(|id| format::cob(&id)).unwrap_or_default(); + (type_name.to_string(), summary, status, id.to_string()) + } + }; + + let id = span::default(format!(" {}", &self.id)); + let seen = if self.seen { + span::blank() + } else { + span::positive(" ● ".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 timestamp = span::timestamp(format::timestamp(&self.timestamp)); + + [ + id.into(), + seen.into(), + type_name.into(), + summary.into(), + kind_id.into(), + status.into(), + timestamp.into(), + ] + } +} diff --git a/src/lib.rs b/src/lib.rs index 92f4957d..718106e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,16 +3,17 @@ use std::hash::Hash; use std::time::Duration; use anyhow::Result; -use radicle::node::notifications::NotificationId; -use serde::ser::{Serialize, SerializeStruct, Serializer}; -use radicle::cob::ObjectId; +use serde::ser::{Serialize, SerializeStruct, Serializer}; use tuirealm::terminal::TerminalBridge; use tuirealm::tui::layout::Rect; use tuirealm::Frame; use tuirealm::{Application, EventListenerCfg, NoUserEvent}; +use radicle::cob::ObjectId; +use radicle::node::notifications::NotificationId; + pub mod cob; pub mod context; pub mod log; @@ -175,8 +176,8 @@ impl Window { } update = tui.update(&mut app)?; - resize = size != self.terminal.raw().size()?; - size = self.terminal.raw().size()?; + resize = size != self.terminal.raw_mut().size()?; + size = self.terminal.raw_mut().size()?; } Ok(tui.exit().unwrap().value)