Skip to content
This repository has been archived by the owner on May 11, 2023. It is now read-only.

Commit

Permalink
issue: Implement comment browser in tui
Browse files Browse the repository at this point in the history
Signed-off-by: Erik Kundt <[email protected]>
  • Loading branch information
erak committed Nov 7, 2022
1 parent 77d8f0c commit d2d9582
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 73 deletions.
2 changes: 1 addition & 1 deletion issue/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ fn create(
let meta: Metadata =
serde_yaml::from_str(&meta).context("failed to parse yaml front-matter")?;

store.create(&project, &meta.title, description.trim(), &meta.labels)?;
store.create(project, &meta.title, description.trim(), &meta.labels)?;
}
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions issue/src/tui.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod app;
pub mod components;
pub mod issue;
85 changes: 30 additions & 55 deletions issue/src/tui/app.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
use anyhow::Result;

use librad::git::storage::ReadOnly;

use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::{AttrValue, Attribute};
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
use tuirealm::{Frame, Sub, SubClause, SubEventClause};

use librad::git::storage::ReadOnly;

use radicle_common::cobs::issue::State as IssueState;
use radicle_common::cobs::issue::*;
use radicle_common::project;

use radicle_terminal_tui as tui;

use tui::components::{ApplicationTitle, Shortcut, ShortcutBar, TabContainer};
use tui::{App, Tui};

use super::components::{CommentList, GlobalListener, IssueList};

use super::issue;
use super::issue::{GroupedIssues, WrappedComment};

/// Messages handled by this tui-application.
#[derive(Debug, Eq, PartialEq)]
pub enum Message {
Expand Down Expand Up @@ -48,23 +51,11 @@ impl Default for Mode {
}
}

#[derive(Default)]
pub struct IssueGroups {
open: Vec<(IssueId, Issue)>,
closed: Vec<(IssueId, Issue)>,
}

impl From<&IssueGroups> for Vec<(IssueId, Issue)> {
fn from(groups: &IssueGroups) -> Self {
[groups.open.clone(), groups.closed.clone()].concat()
}
}

/// App-window used by this application.
#[derive(Default)]
pub struct IssueTui {
/// Issues currently displayed by this tui.
issues: IssueGroups,
issues: GroupedIssues,
/// Represents the active view
mode: Mode,
/// True if application should quit.
Expand All @@ -77,14 +68,14 @@ impl IssueTui {
metadata: &project::Metadata,
store: &IssueStore,
) -> Self {
let issues = match Self::load_issues(storage, metadata, store) {
let issues = match issue::load(storage, metadata, store) {
Ok(issues) => issues,
Err(_) => vec![],
};

Self {
issues: Self::group_issues(&issues),
mode: Mode::Browser,
issues: GroupedIssues::from(&issues),
mode: Mode::default(),
quit: false,
}
}
Expand All @@ -111,43 +102,13 @@ impl IssueTui {
.constraints(
[
Constraint::Length(title_h),
Constraint::Length(container_h - 2),
Constraint::Length(container_h.saturating_sub(2)),
Constraint::Length(shortcuts_h),
]
.as_ref(),
)
.split(area)
}

fn load_issues<S: AsRef<ReadOnly>>(
storage: &S,
metadata: &project::Metadata,
store: &IssueStore,
) -> Result<Vec<(IssueId, Issue)>> {
let mut issues = store.all(&metadata.urn)?;
Self::resolve_issues(storage, &mut issues);
Ok(issues)
}

fn resolve_issues<S: AsRef<ReadOnly>>(storage: &S, issues: &mut Vec<(IssueId, Issue)>) {
let _ = issues
.iter_mut()
.map(|(_, issue)| issue.resolve(&storage).ok())
.collect::<Vec<_>>();
}

fn group_issues(issues: &Vec<(IssueId, Issue)>) -> IssueGroups {
let mut open = issues.clone();
let mut closed = issues.clone();

open.retain(|(_, issue)| issue.state() == IssueState::Open);
closed.retain(|(_, issue)| issue.state() != IssueState::Open);

IssueGroups {
open: open,
closed: closed,
}
}
}

impl Tui<Id, Message> for IssueTui {
Expand Down Expand Up @@ -189,7 +150,7 @@ impl Tui<Id, Message> for IssueTui {
],
)?;

app.mount(Id::Detail, CommentList::<()>::new(vec![]), vec![])?;
app.mount(Id::Detail, CommentList::<()>::new(None, vec![]), vec![])?;

app.mount(
Id::Shortcuts,
Expand Down Expand Up @@ -236,17 +197,31 @@ impl Tui<Id, Message> for IssueTui {
Message::Quit => self.quit = true,
Message::EnterDetail(issue_id) => {
let issues = Vec::<(IssueId, Issue)>::from(&self.issues);
if let Some((_, issue)) = issues.iter().find(|(id, _)| *id == issue_id) {
if let Some((id, issue)) = issues.iter().find(|(id, _)| *id == issue_id) {
let comments = issue
.comments()
.iter()
.map(|comment| comment.clone())
.map(|comment| WrappedComment::Reply {
comment: comment.clone(),
})
.collect::<Vec<_>>();

self.mode = Mode::Detail;

app.remount(Id::Detail, CommentList::new(comments), vec![])
.ok();
let comments = [
vec![WrappedComment::Root {
comment: issue.comment.clone(),
}],
comments,
]
.concat();

app.remount(
Id::Detail,
CommentList::new(Some((*id, issue.clone())), comments),
vec![],
)
.ok();
app.activate(Id::Detail).ok();
}
}
Expand Down
131 changes: 114 additions & 17 deletions issue/src/tui/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,27 @@ use timeago;

use librad::collaborative_objects::ObjectId;

use tui_realm_stdlib::Phantom;
use tui_realm_stdlib::{utils, Phantom};

use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Event, Key, KeyEvent};
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style};
use tuirealm::tui::layout::Rect;
use tuirealm::props::{AttrValue, Attribute, Color, Props, Style, TextSpan};
use tuirealm::tui::layout::{Constraint, Layout, Rect};
use tuirealm::tui::style::Modifier;
use tuirealm::tui::text::{Span, Spans};
use tuirealm::tui::widgets::{List as TuiList, ListItem, ListState as TuiListState};
use tuirealm::{Component, Frame, MockComponent, NoUserEvent, State, StateValue};

use radicle_common::cobs::issue::*;
use radicle_common::cobs::Comment;
use radicle_common::cobs::Timestamp;

use radicle_terminal_tui as tui;
use tui::components::{ApplicationTitle, ShortcutBar, TabContainer};

use tui::components::{ApplicationTitle, ContextBar, ShortcutBar, TabContainer};
use tui::state::ListState;

use super::app::Message;
use super::issue::WrappedComment;

/// Since `terminal-tui` does not know the type of messages that are being
/// passed around in the app, the following handlers need to be implemented for
Expand Down Expand Up @@ -143,7 +146,7 @@ impl IssueList {
}

impl MockComponent for IssueList {
fn view(&mut self, render: &mut Frame, area: Rect) {
fn view(&mut self, frame: &mut Frame, area: Rect) {
let items = self
.issues
.items()
Expand All @@ -161,7 +164,7 @@ impl MockComponent for IssueList {
let mut state: TuiListState = TuiListState::default();

state.select(Some(self.issues.items().selected_index()));
render.render_stateful_widget(list, area, &mut state);
frame.render_stateful_widget(list, area, &mut state);
}

fn query(&self, attr: Attribute) -> Option<AttrValue> {
Expand Down Expand Up @@ -205,34 +208,119 @@ impl Component<Message, NoUserEvent> for IssueList {

pub struct CommentList<R> {
attributes: Props,
comments: ListState<Comment<R>>,
comments: ListState<WrappedComment<R>>,
issue: Option<(IssueId, Issue)>,
}

impl<R> CommentList<R> {
pub fn new(comments: Vec<Comment<R>>) -> Self {
pub fn new(issue: Option<(IssueId, Issue)>, comments: Vec<WrappedComment<R>>) -> Self {
Self {
attributes: Props::default(),
comments: ListState::new(comments),
issue: issue,
}
}

fn items(&self, comment: &Comment<R>) -> ListItem {
let lines = vec![Spans::from(Span::styled(
comment.body.clone(),
Style::default().fg(Color::Rgb(117, 113, 249)),
))];
fn items(&self, comment: &WrappedComment<R>, width: u16) -> ListItem {
let (author, body, reactions, timestamp, indent) = comment.author_info();
let reactions = reactions
.iter()
.map(|(r, _)| format!("{} ", r.emoji))
.collect::<String>();

let lines = [
Self::body(body, indent, width),
vec![
Spans::from(String::new()),
Spans::from(Self::meta(author, reactions, timestamp, indent)),
Spans::from(String::new()),
],
]
.concat();
ListItem::new(lines)
}

fn body<'a>(body: String, indent: u16, width: u16) -> Vec<Spans<'a>> {
let props = Props::default();
let body = TextSpan::new(body).fg(Color::Rgb(150, 150, 150));

let lines = utils::wrap_spans(&[body], (width - indent) as usize, &props)
.iter()
.map(|line| Spans::from(format!("{}{}", whitespaces(indent), line.0[0].content)))
.collect::<Vec<_>>();
lines
}

fn meta<'a>(
author: String,
reactions: String,
timestamp: Timestamp,
indent: u16,
) -> Vec<Span<'a>> {
let fmt = timeago::Formatter::new();
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
let timeago = Duration::from_secs(now - timestamp.as_secs());

vec![
Span::raw(whitespaces(indent)),
Span::styled(
author,
Style::default()
.fg(Color::Rgb(79, 75, 187))
.add_modifier(Modifier::ITALIC),
),
Span::raw(whitespaces(1)),
Span::styled(
fmt.convert(timeago),
Style::default()
.fg(Color::Rgb(70, 70, 70))
.add_modifier(Modifier::ITALIC),
),
Span::raw(whitespaces(1)),
Span::raw(reactions),
]
}
}

impl<R> MockComponent for CommentList<R> {
fn view(&mut self, render: &mut Frame, area: Rect) {
fn view(&mut self, frame: &mut Frame, area: Rect) {
use tuirealm::tui::layout::Direction;

let mut context = match &self.issue {
Some((id, issue)) => ContextBar::new(
"Issue",
&format!("{}", id),
issue.title(),
&issue.author().name(),
&format!("{}", self.comments.items().count()),
),
None => ContextBar::new("Issue", "", "", "", ""),
};
let context_h = context.query(Attribute::Height).unwrap().unwrap_size();
let spacer_h = 1;

let list_h = area.height.saturating_sub(context_h);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(list_h.saturating_sub(spacer_h)),
Constraint::Length(context_h),
Constraint::Length(spacer_h),
]
.as_ref(),
)
.split(area);

let items = self
.comments
.items()
.all()
.iter()
.map(|comment| self.items(comment))
.map(|comment| self.items(comment, area.width))
.collect::<Vec<_>>();

let list = TuiList::new(items)
Expand All @@ -243,7 +331,9 @@ impl<R> MockComponent for CommentList<R> {

let mut state: TuiListState = TuiListState::default();
state.select(Some(self.comments.items().selected_index()));
render.render_stateful_widget(list, area, &mut state);
frame.render_stateful_widget(list, layout[0], &mut state);

context.view(frame, layout[1]);
}

fn query(&self, attr: Attribute) -> Option<AttrValue> {
Expand Down Expand Up @@ -290,3 +380,10 @@ impl<R> Component<Message, NoUserEvent> for CommentList<R> {
}
}
}

pub fn whitespaces(indent: u16) -> String {
match String::from_utf8(vec![b' '; indent as usize]) {
Ok(spaces) => spaces,
Err(_) => String::new(),
}
}
Loading

0 comments on commit d2d9582

Please sign in to comment.