mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-03 18:30:41 +08:00
inbox: Show notifications
This commit is contained in:
@@ -60,6 +60,7 @@ impl Serialize for PatchId {
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Operation,
|
||||
#[allow(dead_code)]
|
||||
Id,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user