diff --git a/radicle-tui/src/app.rs b/radicle-tui/src/app.rs index fb8accb4..8d7f237d 100644 --- a/radicle-tui/src/app.rs +++ b/radicle-tui/src/app.rs @@ -4,6 +4,7 @@ pub mod subscription; use anyhow::Result; +use radicle::cob::issue::{IssueId, Issues}; use radicle::cob::patch::{PatchId, Patches}; use radicle::identity::{Id, Project}; use radicle::profile::Profile; @@ -18,7 +19,7 @@ use radicle_tui::Tui; use page::{HomeView, PatchView}; -use self::page::PageStack; +use self::page::{IssuePage, PageStack}; #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum HomeCid { @@ -35,10 +36,16 @@ pub enum PatchCid { Files, } +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum IssueCid { + List, +} + /// All component ids known to this application. #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub enum Cid { Home(HomeCid), + Issue(IssueCid), Patch(PatchCid), GlobalListener, } @@ -47,6 +54,12 @@ pub enum Cid { #[derive(Debug, Eq, PartialEq)] pub enum HomeMessage {} +#[derive(Debug, Eq, PartialEq)] +pub enum IssueMessage { + Show(IssueId), + Leave, +} + #[derive(Debug, Eq, PartialEq)] pub enum PatchMessage { Show(PatchId), @@ -56,6 +69,7 @@ pub enum PatchMessage { #[derive(Debug, Eq, PartialEq)] pub enum Message { Home(HomeMessage), + Issue(IssueMessage), Patch(PatchMessage), NavigationChanged(u16), Tick, @@ -128,6 +142,32 @@ impl App { )) } } + + fn view_issue( + &mut self, + app: &mut Application, + id: IssueId, + theme: &Theme, + ) -> Result<()> { + let repo = self + .context + .profile + .storage + .repository(self.context.id) + .unwrap(); + let issues = Issues::open(&repo).unwrap(); + + if let Some(issue) = issues.get(&id)? { + let view = Box::new(IssuePage::new((id, issue))); + self.pages.push(view, app, &self.context, theme)?; + + Ok(()) + } else { + Err(anyhow::anyhow!( + "Could not mount 'page::IssueView'. Issue not found." + )) + } + } } impl Tui for App { @@ -153,6 +193,9 @@ impl Tui for App { let theme = theme::default_dark(); for message in messages { match message { + Message::Issue(IssueMessage::Show(id)) => { + self.view_issue(app, id, &theme)?; + } Message::Patch(PatchMessage::Show(id)) => { self.view_patch(app, id, &theme)?; } diff --git a/radicle-tui/src/app/event.rs b/radicle-tui/src/app/event.rs index a6719b35..55e2f538 100644 --- a/radicle-tui/src/app/event.rs +++ b/radicle-tui/src/app/event.rs @@ -10,7 +10,7 @@ use radicle_tui::ui::widget::patch; use radicle_tui::ui::widget::Widget; -use super::{Message, PatchMessage}; +use super::{IssueMessage, Message, PatchMessage}; /// Since the framework does not know the type of messages that are being /// passed around in the app, the following handlers need to be implemented for @@ -83,7 +83,9 @@ impl tuirealm::Component for Widget { } Event::Keyboard(KeyEvent { code: Key::Enter, .. - }) => Some(Message::Tick), + }) => self + .selected_item() + .map(|item| Message::Issue(IssueMessage::Show(item.id()))), _ => None, } } diff --git a/radicle-tui/src/app/page.rs b/radicle-tui/src/app/page.rs index c2d481fc..54591379 100644 --- a/radicle-tui/src/app/page.rs +++ b/radicle-tui/src/app/page.rs @@ -1,13 +1,16 @@ use anyhow::Result; -use radicle::cob::patch::{Patch, PatchId}; +use radicle::cob::{ + issue::{Issue, IssueId}, + patch::{Patch, PatchId}, +}; use tuirealm::{Frame, NoUserEvent}; use radicle_tui::ui::layout; use radicle_tui::ui::theme::Theme; use radicle_tui::ui::widget; -use super::{subscription, Application, Cid, Context, HomeCid, Message, PatchCid}; +use super::{subscription, Application, Cid, Context, HomeCid, IssueCid, Message, PatchCid}; /// `tuirealm`'s event and prop system is designed to work with flat component hierarchies. /// Building deep nested component hierarchies would need a lot more additional effort to @@ -113,6 +116,63 @@ impl ViewPage for HomeView { } } +/// +/// Issue detail page +/// +pub struct IssuePage { + active_component: Cid, + issue: (IssueId, Issue), +} + +impl IssuePage { + pub fn new(issue: (IssueId, Issue)) -> Self { + IssuePage { + active_component: Cid::Issue(IssueCid::List), + issue, + } + } +} + +impl ViewPage for IssuePage { + fn mount( + &self, + app: &mut Application, + context: &Context, + theme: &Theme, + ) -> Result<()> { + let (id, issue) = &self.issue; + let list = widget::issue::list(theme, (*id, issue), &context.profile).to_boxed(); + + app.remount(Cid::Issue(IssueCid::List), list, vec![])?; + app.active(&self.active_component)?; + + Ok(()) + } + + fn unmount(&self, app: &mut Application) -> Result<()> { + app.umount(&Cid::Issue(IssueCid::List))?; + Ok(()) + } + + fn update( + &mut self, + app: &mut Application, + _message: Message, + ) -> Result<()> { + app.active(&self.active_component)?; + + Ok(()) + } + + fn view(&mut self, app: &mut Application, frame: &mut Frame) { + let area = frame.size(); + let layout = layout::default_page(area); + + app.view(&Cid::Patch(PatchCid::Navigation), frame, layout[0]); + app.view(&self.active_component, frame, layout[1]); + } +} + /// /// Patch detail page /// diff --git a/radicle-tui/src/ui/cob.rs b/radicle-tui/src/ui/cob.rs index e5ae1729..3ef78f97 100644 --- a/radicle-tui/src/ui/cob.rs +++ b/radicle-tui/src/ui/cob.rs @@ -139,6 +139,12 @@ pub struct IssueItem { timestamp: Timestamp, } +impl IssueItem { + pub fn id(&self) -> IssueId { + self.id + } +} + impl TryFrom<(&Profile, &Repository, IssueId, Issue)> for IssueItem { type Error = anyhow::Error; diff --git a/radicle-tui/src/ui/widget.rs b/radicle-tui/src/ui/widget.rs index 305aa0fd..f1f8b5cd 100644 --- a/radicle-tui/src/ui/widget.rs +++ b/radicle-tui/src/ui/widget.rs @@ -1,5 +1,6 @@ pub mod common; pub mod home; +pub mod issue; pub mod patch; use std::ops::Deref; diff --git a/radicle-tui/src/ui/widget/issue.rs b/radicle-tui/src/ui/widget/issue.rs new file mode 100644 index 00000000..dffdda21 --- /dev/null +++ b/radicle-tui/src/ui/widget/issue.rs @@ -0,0 +1,59 @@ +use radicle_cli::terminal::format; + +use radicle::cob::issue::Issue; +use radicle::cob::issue::IssueId; +use radicle::Profile; +use tuirealm::props::Color; + +use super::common; +use super::Widget; + +use crate::ui::cob; +use crate::ui::theme::Theme; +use crate::ui::widget::common::context::ContextBar; +use crate::ui::widget::patch::Activity; + +pub fn list(theme: &Theme, issue: (IssueId, &Issue), profile: &Profile) -> Widget { + let (id, issue) = issue; + let shortcuts = common::shortcuts( + theme, + vec![ + common::shortcut(theme, "esc", "back"), + common::shortcut(theme, "q", "quit"), + ], + ); + let context = context(theme, (id, issue), profile); + + let not_implemented = common::label("not implemented").foreground(theme.colors.default_fg); + let activity = Activity::new(not_implemented, context, shortcuts); + + Widget::new(activity) +} + +pub fn context(theme: &Theme, issue: (IssueId, &Issue), profile: &Profile) -> Widget { + let (id, issue) = issue; + let is_you = *issue.author().id() == profile.did(); + + let id = format::cob(&id); + let title = issue.title(); + let author = cob::format_author(issue.author().id(), is_you); + let comments = issue.comments().count(); + + let context = common::label(" issue ").background(theme.colors.context_badge_bg); + let id = common::label(&format!(" {id} ")) + .foreground(theme.colors.context_id_fg) + .background(theme.colors.context_id_bg); + let title = common::label(&format!(" {title} ")) + .foreground(theme.colors.default_fg) + .background(theme.colors.context_bg); + let author = common::label(&format!(" {author} ")) + .foreground(theme.colors.context_id_author_fg) + .background(theme.colors.context_bg); + let comments = common::label(&format!(" {comments} ")) + .foreground(Color::Rgb(70, 70, 70)) + .background(theme.colors.context_light_bg); + + let context_bar = ContextBar::new(context, id, author, title, comments); + + Widget::new(context_bar).height(1) +}