@@ -5,24 +5,27 @@ use timeago;
5
5
6
6
use librad:: collaborative_objects:: ObjectId ;
7
7
8
- use tui_realm_stdlib:: Phantom ;
8
+ use tui_realm_stdlib:: { utils , Phantom } ;
9
9
10
10
use tuirealm:: command:: { Cmd , CmdResult , Direction } ;
11
11
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 } ;
14
14
use tuirealm:: tui:: style:: Modifier ;
15
15
use tuirealm:: tui:: text:: { Span , Spans } ;
16
16
use tuirealm:: tui:: widgets:: { List as TuiList , ListItem , ListState as TuiListState } ;
17
17
use tuirealm:: { Component , Frame , MockComponent , NoUserEvent , State , StateValue } ;
18
18
19
19
use radicle_common:: cobs:: issue:: * ;
20
- use radicle_common:: cobs:: Comment ;
20
+ use radicle_common:: cobs:: Timestamp ;
21
+
21
22
use radicle_terminal_tui as tui;
22
- use tui:: components:: { ApplicationTitle , ShortcutBar , TabContainer } ;
23
+
24
+ use tui:: components:: { ApplicationTitle , ContextBar , ShortcutBar , TabContainer } ;
23
25
use tui:: state:: ListState ;
24
26
25
27
use super :: app:: Message ;
28
+ use super :: issue:: WrappedComment ;
26
29
27
30
/// Since `terminal-tui` does not know the type of messages that are being
28
31
/// passed around in the app, the following handlers need to be implemented for
@@ -143,7 +146,7 @@ impl IssueList {
143
146
}
144
147
145
148
impl MockComponent for IssueList {
146
- fn view ( & mut self , render : & mut Frame , area : Rect ) {
149
+ fn view ( & mut self , frame : & mut Frame , area : Rect ) {
147
150
let items = self
148
151
. issues
149
152
. items ( )
@@ -161,7 +164,7 @@ impl MockComponent for IssueList {
161
164
let mut state: TuiListState = TuiListState :: default ( ) ;
162
165
163
166
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) ;
165
168
}
166
169
167
170
fn query ( & self , attr : Attribute ) -> Option < AttrValue > {
@@ -205,34 +208,119 @@ impl Component<Message, NoUserEvent> for IssueList {
205
208
206
209
pub struct CommentList < R > {
207
210
attributes : Props ,
208
- comments : ListState < Comment < R > > ,
211
+ comments : ListState < WrappedComment < R > > ,
212
+ issue : Option < ( IssueId , Issue ) > ,
209
213
}
210
214
211
215
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 {
213
217
Self {
214
218
attributes : Props :: default ( ) ,
215
219
comments : ListState :: new ( comments) ,
220
+ issue : issue,
216
221
}
217
222
}
218
223
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 ( ) ;
224
240
ListItem :: new ( lines)
225
241
}
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
+ }
226
286
}
227
287
228
288
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
+
230
318
let items = self
231
319
. comments
232
320
. items ( )
233
321
. all ( )
234
322
. iter ( )
235
- . map ( |comment| self . items ( comment) )
323
+ . map ( |comment| self . items ( comment, area . width ) )
236
324
. collect :: < Vec < _ > > ( ) ;
237
325
238
326
let list = TuiList :: new ( items)
@@ -243,7 +331,9 @@ impl<R> MockComponent for CommentList<R> {
243
331
244
332
let mut state: TuiListState = TuiListState :: default ( ) ;
245
333
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 ] ) ;
247
337
}
248
338
249
339
fn query ( & self , attr : Attribute ) -> Option < AttrValue > {
@@ -290,3 +380,10 @@ impl<R> Component<Message, NoUserEvent> for CommentList<R> {
290
380
}
291
381
}
292
382
}
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