inbox: Show notifications

This commit is contained in:
Erik Kundt
2024-02-16 16:30:07 +01:00
parent e0ddf6906a
commit f23cc04759
8 changed files with 291 additions and 24 deletions
+1
View File
@@ -60,6 +60,7 @@ impl Serialize for PatchId {
pub enum Mode {
#[default]
Operation,
#[allow(dead_code)]
Id,
}
+1 -1
View File
@@ -36,7 +36,7 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<OperationSelect> {
fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
let mut submit = || -> Option<radicle::cob::patch::PatchId> {
match self.perform(Cmd::Submit) {
CmdResult::Submit(state) => None,
CmdResult::Submit(_) => None,
_ => None,
}
};
+5 -5
View File
@@ -3,16 +3,16 @@ use std::collections::HashMap;
use anyhow::Result;
use tui::ui::state::ItemState;
use tuirealm::{AttrValue, Attribute, Frame, NoUserEvent, Sub, SubClause};
use tuirealm::{AttrValue, Attribute, Frame, NoUserEvent};
use radicle_tui as tui;
use tui::cob::inbox::Filter;
use tui::context::Context;
use tui::ui::layout;
use tui::ui::theme::Theme;
use tui::ui::widget::context::{Progress, Shortcuts};
use tui::ui::widget::Widget;
use tui::ui::{layout, subscription};
use tui::ViewPage;
use super::{ui, Application, Cid, ListCid, Message};
@@ -84,7 +84,7 @@ impl ViewPage<Cid, Message> for ListView {
context: &Context,
theme: &Theme,
) -> Result<()> {
let browser = ui::operation_select(theme, context, self.filter.clone(), None).to_boxed();
let browser = ui::operation_select(theme, context, self.filter.clone()).to_boxed();
self.shortcuts = browser.as_ref().shortcuts();
app.remount(Cid::List(ListCid::NotificationBrowser), browser, vec![])?;
@@ -136,11 +136,11 @@ impl ViewPage<Cid, Message> for ListView {
app.view(&Cid::List(ListCid::Shortcuts), frame, layout.shortcuts);
}
fn subscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
fn subscribe(&self, _app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
Ok(())
}
fn unsubscribe(&self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
fn unsubscribe(&self, _app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
Ok(())
}
}
+65 -10
View File
@@ -1,7 +1,9 @@
use std::collections::HashMap;
use radicle::issue::{Issue, IssueId};
use radicle::node::notifications::Notification;
use tui::ui::cob::NotificationItem;
use tui::ui::widget::list::{ColumnWidth, Table};
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::tui::layout::Rect;
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};
@@ -17,21 +19,74 @@ use tui::ui::widget::{Widget, WidgetComponent};
use super::ListCid;
pub struct NotificationBrowser {}
pub struct NotificationBrowser {
items: Vec<NotificationItem>,
table: Widget<Table<NotificationItem, 7>>,
}
impl NotificationBrowser {
pub fn new(theme: &Theme, context: &Context, selected: Option<Notification>) -> Self {
let header = [
label::header(""),
label::header(" ● "),
label::header("Type"),
label::header("Summary"),
label::header("ID"),
label::header("Status"),
label::header("Updated"),
];
let widths = [
ColumnWidth::Fixed(5),
ColumnWidth::Fixed(3),
ColumnWidth::Fixed(6),
ColumnWidth::Grow,
ColumnWidth::Fixed(15),
ColumnWidth::Fixed(10),
ColumnWidth::Fixed(15),
];
let mut items = vec![];
for notification in context.notifications() {
if let Ok(item) =
NotificationItem::try_from((context.repository(), notification.clone()))
{
items.push(item);
}
}
let selected = match selected {
Some(notif) => {
Some(NotificationItem::try_from((context.repository(), notif.clone())).unwrap())
}
_ => items.first().cloned(),
};
let table = Widget::new(Table::new(&items, selected, header, widths, theme.clone()));
Self { items, table }
}
pub fn items(&self) -> &Vec<NotificationItem> {
&self.items
}
}
impl WidgetComponent for NotificationBrowser {
fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
let focus = properties
.get_or(Attribute::Focus, AttrValue::Flag(false))
.unwrap_flag();
self.table.attr(Attribute::Focus, AttrValue::Flag(focus));
self.table.view(frame, area);
}
fn state(&self) -> State {
State::None
self.table.state()
}
fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
CmdResult::None
self.table.perform(cmd)
}
}
@@ -81,18 +136,18 @@ impl WidgetComponent for OperationSelect {
pub fn operation_select(
theme: &Theme,
context: &Context,
filter: Filter,
selected: Option<(IssueId, Issue)>,
_filter: Filter,
selected: Option<Notification>,
) -> Widget<OperationSelect> {
let browser = Widget::new(NotificationBrowser {});
let browser = Widget::new(NotificationBrowser::new(theme, context, selected));
Widget::new(OperationSelect::new(theme.clone(), browser))
}
pub fn browse_context(
context: &Context,
_context: &Context,
_theme: &Theme,
filter: Filter,
_filter: Filter,
progress: Progress,
) -> Widget<ContextBar> {
let context = label::reversable("/").style(style::magenta_reversed());
@@ -101,7 +156,7 @@ pub fn browse_context(
let progress = label::reversable(&progress.to_string()).style(style::magenta_reversed());
let spacer = label::default("");
let divider = label::default(" | ");
let _divider = label::default(" | ");
let context_bar = ContextBar::new(
label::group(&[context]),
+21
View File
@@ -1,2 +1,23 @@
use anyhow::Result;
use radicle::node::notifications::Notification;
use radicle::storage::git::Repository;
use radicle::Profile;
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub struct Filter {}
pub fn all(repository: &Repository, profile: &Profile) -> Result<Vec<Notification>> {
let all = profile
.notifications_mut()?
.by_repo(&repository.id, "timestamp")?
.collect::<Vec<_>>();
let mut notifications = vec![];
for n in all {
let n = n?;
notifications.push(n);
}
Ok(notifications)
}
+12
View File
@@ -8,6 +8,8 @@ use radicle::identity::{Project, RepoId};
use radicle::profile::env::RAD_PASSPHRASE;
use radicle::storage::git::Repository;
use radicle::storage::{ReadRepository, ReadStorage};
use radicle::node::notifications::*;
use radicle::Profile;
use radicle_term as term;
@@ -15,6 +17,8 @@ use term::{passphrase, spinner, Passphrase};
use inquire::validator;
use crate::cob::inbox;
/// Git revision parameter. Supports extended SHA-1 syntax.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rev(String);
@@ -40,6 +44,7 @@ pub struct Context {
repository: Repository,
issues: Option<Vec<(IssueId, Issue)>>,
patches: Option<Vec<(PatchId, Patch)>>,
notifications: Vec<Notification>,
signer: Option<Box<dyn Signer>>,
}
@@ -47,6 +52,8 @@ impl Context {
pub fn new(profile: Profile, rid: RepoId) -> Result<Self, anyhow::Error> {
let repository = profile.storage.repository(rid).unwrap();
let project = repository.identity_doc()?.project()?;
let notifications = inbox::all(&repository, &profile)?;
let issues = None;
let patches = None;
let signer = None;
@@ -58,6 +65,7 @@ impl Context {
repository,
issues,
patches,
notifications,
signer,
})
}
@@ -103,6 +111,10 @@ impl Context {
&self.patches
}
pub fn notifications(&self) -> &Vec<Notification> {
&self.notifications
}
#[allow(clippy::borrowed_box)]
pub fn signer(&self) -> &Option<Box<dyn Signer>> {
&self.signer
+182 -8
View File
@@ -1,21 +1,24 @@
pub mod format;
use anyhow::anyhow;
use radicle_surf;
use tuirealm::props::{Color, Style};
use tuirealm::tui::text::Line;
use tuirealm::tui::widgets::Cell;
use radicle::node::{Alias, AliasStore};
use radicle::prelude::Did;
use radicle::storage::git::Repository;
use radicle::storage::{Oid, ReadRepository};
use radicle::Profile;
use radicle::cob::issue::{self, Issue, IssueId};
use radicle::cob::patch::{self, Patch, PatchId};
use radicle::cob::{Label, Timestamp};
use radicle::cob::{Label, ObjectId, Timestamp};
use radicle::issue::Issues;
use radicle::node::notifications::{Notification, NotificationId, NotificationKind};
use radicle::node::{Alias, AliasStore};
use radicle::patch::Patches;
use radicle::prelude::Did;
use radicle::storage::git::Repository;
use radicle::storage::{Oid, ReadRepository, RefUpdate};
use radicle::{cob, Profile};
use crate::ui::theme::Theme;
use crate::ui::widget::list::{ListItem, TableItem};
@@ -380,6 +383,177 @@ impl PartialEq for IssueItem {
}
}
//////////////////////////////////////////////////////
#[derive(Clone)]
pub enum NotificationKindItem {
Branch {
name: String,
summary: String,
status: String,
id: Option<ObjectId>,
},
Cob {
type_name: String,
summary: String,
status: String,
id: Option<ObjectId>,
},
}
impl TryFrom<(&Repository, NotificationKind, RefUpdate)> for NotificationKindItem {
type Error = anyhow::Error;
fn try_from(value: (&Repository, NotificationKind, RefUpdate)) -> Result<Self, Self::Error> {
let (repo, kind, update) = value;
let issues = Issues::open(repo)?;
let patches = Patches::open(repo)?;
match kind {
NotificationKind::Branch { name } => {
let (head, message) = if let Some(head) = update.new() {
let message = repo.commit(head)?.summary().unwrap_or_default().to_owned();
(Some(head), message)
} else {
(None, String::new())
};
let status = match 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 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)]
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 NotificationItem {
pub fn id(&self) -> &NotificationId {
&self.id
}
pub fn seen(&self) -> bool {
self.seen
}
pub fn kind(&self) -> &NotificationKindItem {
&self.kind
}
pub fn timestamp(&self) -> &Timestamp {
&self.timestamp
}
}
impl TableItem<7> for NotificationItem {
fn row(&self, _theme: &Theme, highlight: bool) -> [Cell; 7] {
let seen = if self.seen {
label::blank()
} else {
label::positive(" ● ")
};
let (type_name, summary, status, 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 timestamp = if highlight {
label::reversed(&format::timestamp(&self.timestamp))
} else {
label::timestamp(&format::timestamp(&self.timestamp))
};
[
label::default(&format!(" {}", &self.id)).into(),
seen.into(),
label::alias(&type_name).into(),
label::default(summary).into(),
label::id(&id).into(),
label::default(status).into(),
timestamp.into(),
]
}
}
impl TryFrom<(&Repository, Notification)> for NotificationItem {
type Error = anyhow::Error;
fn try_from(value: (&Repository, Notification)) -> Result<Self, Self::Error> {
let (repo, notification) = value;
let kind = NotificationKindItem::try_from((repo, notification.kind, notification.update))?;
Ok(NotificationItem {
id: notification.id,
seen: notification.status.is_read(),
kind,
timestamp: notification.timestamp.into(),
})
}
}
impl PartialEq for NotificationItem {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
pub fn format_patch_state(state: &patch::State) -> (String, Color) {
match state {
patch::State::Open { conflicts: _ } => (" ● ".into(), Color::Green),
+4
View File
@@ -8,6 +8,10 @@ use crate::ui::layout;
use crate::ui::theme::style;
use crate::ui::widget::{Widget, WidgetComponent};
pub fn blank() -> Widget<Label> {
default("")
}
pub fn default(content: &str) -> Widget<Label> {
// TODO: Remove when size constraints are implemented
let width = content.chars().count() as u16;