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

Commit d2d9582

Browse files
committed
issue: Implement comment browser in tui
Signed-off-by: Erik Kundt <[email protected]>
1 parent 77d8f0c commit d2d9582

File tree

5 files changed

+229
-73
lines changed

5 files changed

+229
-73
lines changed

issue/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ fn create(
259259
let meta: Metadata =
260260
serde_yaml::from_str(&meta).context("failed to parse yaml front-matter")?;
261261

262-
store.create(&project, &meta.title, description.trim(), &meta.labels)?;
262+
store.create(project, &meta.title, description.trim(), &meta.labels)?;
263263
}
264264
Ok(())
265265
}

issue/src/tui.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub mod app;
22
pub mod components;
3+
pub mod issue;

issue/src/tui/app.rs

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
use anyhow::Result;
22

3+
use librad::git::storage::ReadOnly;
4+
35
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
46
use tuirealm::props::{AttrValue, Attribute};
57
use tuirealm::tui::layout::{Constraint, Direction, Layout, Rect};
68
use tuirealm::{Frame, Sub, SubClause, SubEventClause};
79

8-
use librad::git::storage::ReadOnly;
9-
10-
use radicle_common::cobs::issue::State as IssueState;
1110
use radicle_common::cobs::issue::*;
1211
use radicle_common::project;
1312

1413
use radicle_terminal_tui as tui;
14+
1515
use tui::components::{ApplicationTitle, Shortcut, ShortcutBar, TabContainer};
1616
use tui::{App, Tui};
1717

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

20+
use super::issue;
21+
use super::issue::{GroupedIssues, WrappedComment};
22+
2023
/// Messages handled by this tui-application.
2124
#[derive(Debug, Eq, PartialEq)]
2225
pub enum Message {
@@ -48,23 +51,11 @@ impl Default for Mode {
4851
}
4952
}
5053

51-
#[derive(Default)]
52-
pub struct IssueGroups {
53-
open: Vec<(IssueId, Issue)>,
54-
closed: Vec<(IssueId, Issue)>,
55-
}
56-
57-
impl From<&IssueGroups> for Vec<(IssueId, Issue)> {
58-
fn from(groups: &IssueGroups) -> Self {
59-
[groups.open.clone(), groups.closed.clone()].concat()
60-
}
61-
}
62-
6354
/// App-window used by this application.
6455
#[derive(Default)]
6556
pub struct IssueTui {
6657
/// Issues currently displayed by this tui.
67-
issues: IssueGroups,
58+
issues: GroupedIssues,
6859
/// Represents the active view
6960
mode: Mode,
7061
/// True if application should quit.
@@ -77,14 +68,14 @@ impl IssueTui {
7768
metadata: &project::Metadata,
7869
store: &IssueStore,
7970
) -> Self {
80-
let issues = match Self::load_issues(storage, metadata, store) {
71+
let issues = match issue::load(storage, metadata, store) {
8172
Ok(issues) => issues,
8273
Err(_) => vec![],
8374
};
8475

8576
Self {
86-
issues: Self::group_issues(&issues),
87-
mode: Mode::Browser,
77+
issues: GroupedIssues::from(&issues),
78+
mode: Mode::default(),
8879
quit: false,
8980
}
9081
}
@@ -111,43 +102,13 @@ impl IssueTui {
111102
.constraints(
112103
[
113104
Constraint::Length(title_h),
114-
Constraint::Length(container_h - 2),
105+
Constraint::Length(container_h.saturating_sub(2)),
115106
Constraint::Length(shortcuts_h),
116107
]
117108
.as_ref(),
118109
)
119110
.split(area)
120111
}
121-
122-
fn load_issues<S: AsRef<ReadOnly>>(
123-
storage: &S,
124-
metadata: &project::Metadata,
125-
store: &IssueStore,
126-
) -> Result<Vec<(IssueId, Issue)>> {
127-
let mut issues = store.all(&metadata.urn)?;
128-
Self::resolve_issues(storage, &mut issues);
129-
Ok(issues)
130-
}
131-
132-
fn resolve_issues<S: AsRef<ReadOnly>>(storage: &S, issues: &mut Vec<(IssueId, Issue)>) {
133-
let _ = issues
134-
.iter_mut()
135-
.map(|(_, issue)| issue.resolve(&storage).ok())
136-
.collect::<Vec<_>>();
137-
}
138-
139-
fn group_issues(issues: &Vec<(IssueId, Issue)>) -> IssueGroups {
140-
let mut open = issues.clone();
141-
let mut closed = issues.clone();
142-
143-
open.retain(|(_, issue)| issue.state() == IssueState::Open);
144-
closed.retain(|(_, issue)| issue.state() != IssueState::Open);
145-
146-
IssueGroups {
147-
open: open,
148-
closed: closed,
149-
}
150-
}
151112
}
152113

153114
impl Tui<Id, Message> for IssueTui {
@@ -189,7 +150,7 @@ impl Tui<Id, Message> for IssueTui {
189150
],
190151
)?;
191152

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

194155
app.mount(
195156
Id::Shortcuts,
@@ -236,17 +197,31 @@ impl Tui<Id, Message> for IssueTui {
236197
Message::Quit => self.quit = true,
237198
Message::EnterDetail(issue_id) => {
238199
let issues = Vec::<(IssueId, Issue)>::from(&self.issues);
239-
if let Some((_, issue)) = issues.iter().find(|(id, _)| *id == issue_id) {
200+
if let Some((id, issue)) = issues.iter().find(|(id, _)| *id == issue_id) {
240201
let comments = issue
241202
.comments()
242203
.iter()
243-
.map(|comment| comment.clone())
204+
.map(|comment| WrappedComment::Reply {
205+
comment: comment.clone(),
206+
})
244207
.collect::<Vec<_>>();
245208

246209
self.mode = Mode::Detail;
247210

248-
app.remount(Id::Detail, CommentList::new(comments), vec![])
249-
.ok();
211+
let comments = [
212+
vec![WrappedComment::Root {
213+
comment: issue.comment.clone(),
214+
}],
215+
comments,
216+
]
217+
.concat();
218+
219+
app.remount(
220+
Id::Detail,
221+
CommentList::new(Some((*id, issue.clone())), comments),
222+
vec![],
223+
)
224+
.ok();
250225
app.activate(Id::Detail).ok();
251226
}
252227
}

issue/src/tui/components.rs

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,27 @@ use timeago;
55

66
use librad::collaborative_objects::ObjectId;
77

8-
use tui_realm_stdlib::Phantom;
8+
use tui_realm_stdlib::{utils, Phantom};
99

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

1919
use radicle_common::cobs::issue::*;
20-
use radicle_common::cobs::Comment;
20+
use radicle_common::cobs::Timestamp;
21+
2122
use radicle_terminal_tui as tui;
22-
use tui::components::{ApplicationTitle, ShortcutBar, TabContainer};
23+
24+
use tui::components::{ApplicationTitle, ContextBar, ShortcutBar, TabContainer};
2325
use tui::state::ListState;
2426

2527
use super::app::Message;
28+
use super::issue::WrappedComment;
2629

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

145148
impl MockComponent for IssueList {
146-
fn view(&mut self, render: &mut Frame, area: Rect) {
149+
fn view(&mut self, frame: &mut Frame, area: Rect) {
147150
let items = self
148151
.issues
149152
.items()
@@ -161,7 +164,7 @@ impl MockComponent for IssueList {
161164
let mut state: TuiListState = TuiListState::default();
162165

163166
state.select(Some(self.issues.items().selected_index()));
164-
render.render_stateful_widget(list, area, &mut state);
167+
frame.render_stateful_widget(list, area, &mut state);
165168
}
166169

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

206209
pub struct CommentList<R> {
207210
attributes: Props,
208-
comments: ListState<Comment<R>>,
211+
comments: ListState<WrappedComment<R>>,
212+
issue: Option<(IssueId, Issue)>,
209213
}
210214

211215
impl<R> CommentList<R> {
212-
pub fn new(comments: Vec<Comment<R>>) -> Self {
216+
pub fn new(issue: Option<(IssueId, Issue)>, comments: Vec<WrappedComment<R>>) -> Self {
213217
Self {
214218
attributes: Props::default(),
215219
comments: ListState::new(comments),
220+
issue: issue,
216221
}
217222
}
218223

219-
fn items(&self, comment: &Comment<R>) -> ListItem {
220-
let lines = vec![Spans::from(Span::styled(
221-
comment.body.clone(),
222-
Style::default().fg(Color::Rgb(117, 113, 249)),
223-
))];
224+
fn items(&self, comment: &WrappedComment<R>, width: u16) -> ListItem {
225+
let (author, body, reactions, timestamp, indent) = comment.author_info();
226+
let reactions = reactions
227+
.iter()
228+
.map(|(r, _)| format!("{} ", r.emoji))
229+
.collect::<String>();
230+
231+
let lines = [
232+
Self::body(body, indent, width),
233+
vec![
234+
Spans::from(String::new()),
235+
Spans::from(Self::meta(author, reactions, timestamp, indent)),
236+
Spans::from(String::new()),
237+
],
238+
]
239+
.concat();
224240
ListItem::new(lines)
225241
}
242+
243+
fn body<'a>(body: String, indent: u16, width: u16) -> Vec<Spans<'a>> {
244+
let props = Props::default();
245+
let body = TextSpan::new(body).fg(Color::Rgb(150, 150, 150));
246+
247+
let lines = utils::wrap_spans(&[body], (width - indent) as usize, &props)
248+
.iter()
249+
.map(|line| Spans::from(format!("{}{}", whitespaces(indent), line.0[0].content)))
250+
.collect::<Vec<_>>();
251+
lines
252+
}
253+
254+
fn meta<'a>(
255+
author: String,
256+
reactions: String,
257+
timestamp: Timestamp,
258+
indent: u16,
259+
) -> Vec<Span<'a>> {
260+
let fmt = timeago::Formatter::new();
261+
let now = SystemTime::now()
262+
.duration_since(UNIX_EPOCH)
263+
.unwrap()
264+
.as_secs();
265+
let timeago = Duration::from_secs(now - timestamp.as_secs());
266+
267+
vec![
268+
Span::raw(whitespaces(indent)),
269+
Span::styled(
270+
author,
271+
Style::default()
272+
.fg(Color::Rgb(79, 75, 187))
273+
.add_modifier(Modifier::ITALIC),
274+
),
275+
Span::raw(whitespaces(1)),
276+
Span::styled(
277+
fmt.convert(timeago),
278+
Style::default()
279+
.fg(Color::Rgb(70, 70, 70))
280+
.add_modifier(Modifier::ITALIC),
281+
),
282+
Span::raw(whitespaces(1)),
283+
Span::raw(reactions),
284+
]
285+
}
226286
}
227287

228288
impl<R> MockComponent for CommentList<R> {
229-
fn view(&mut self, render: &mut Frame, area: Rect) {
289+
fn view(&mut self, frame: &mut Frame, area: Rect) {
290+
use tuirealm::tui::layout::Direction;
291+
292+
let mut context = match &self.issue {
293+
Some((id, issue)) => ContextBar::new(
294+
"Issue",
295+
&format!("{}", id),
296+
issue.title(),
297+
&issue.author().name(),
298+
&format!("{}", self.comments.items().count()),
299+
),
300+
None => ContextBar::new("Issue", "", "", "", ""),
301+
};
302+
let context_h = context.query(Attribute::Height).unwrap().unwrap_size();
303+
let spacer_h = 1;
304+
305+
let list_h = area.height.saturating_sub(context_h);
306+
let layout = Layout::default()
307+
.direction(Direction::Vertical)
308+
.constraints(
309+
[
310+
Constraint::Length(list_h.saturating_sub(spacer_h)),
311+
Constraint::Length(context_h),
312+
Constraint::Length(spacer_h),
313+
]
314+
.as_ref(),
315+
)
316+
.split(area);
317+
230318
let items = self
231319
.comments
232320
.items()
233321
.all()
234322
.iter()
235-
.map(|comment| self.items(comment))
323+
.map(|comment| self.items(comment, area.width))
236324
.collect::<Vec<_>>();
237325

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

244332
let mut state: TuiListState = TuiListState::default();
245333
state.select(Some(self.comments.items().selected_index()));
246-
render.render_stateful_widget(list, area, &mut state);
334+
frame.render_stateful_widget(list, layout[0], &mut state);
335+
336+
context.view(frame, layout[1]);
247337
}
248338

249339
fn query(&self, attr: Attribute) -> Option<AttrValue> {
@@ -290,3 +380,10 @@ impl<R> Component<Message, NoUserEvent> for CommentList<R> {
290380
}
291381
}
292382
}
383+
384+
pub fn whitespaces(indent: u16) -> String {
385+
match String::from_utf8(vec![b' '; indent as usize]) {
386+
Ok(spaces) => spaces,
387+
Err(_) => String::new(),
388+
}
389+
}

0 commit comments

Comments
 (0)