@@ -28,7 +28,7 @@ export const CommentDialog = props =>
28
28
29
29
const DIALOG_WIDTH = 420 ;
30
30
const DIALOG_TRANSITION_DURATION = 0.25 ;
31
- const REPLY_TRANSITION_DELAY = 0.25 ;
31
+ const REPLY_TRANSITION_DELAY = 0.5 ;
32
32
33
33
export const Dialog : React . FC = ( ) => {
34
34
const { state } = useOvermind ( ) ;
@@ -135,12 +135,11 @@ export const Dialog: React.FC = () => {
135
135
setEditing = { setEditing }
136
136
hasReplies = { replies . length }
137
137
/>
138
- { replies . length ? (
139
- < Replies
140
- replies = { replies }
141
- repliesRenderedCallback = { ( ) => setRepliesRendered ( true ) }
142
- />
143
- ) : null }
138
+
139
+ < Replies
140
+ replies = { replies }
141
+ repliesRenderedCallback = { ( ) => setRepliesRendered ( true ) }
142
+ />
144
143
</ Element >
145
144
< AddReply
146
145
comment = { comment }
@@ -365,43 +364,162 @@ const CommentBody = ({ comment, editing, setEditing, hasReplies }) => {
365
364
} ;
366
365
367
366
const Replies = ( { replies, repliesRenderedCallback } ) => {
368
- const [ isAnimating , setAnimating ] = React . useState ( true ) ;
369
- const repliesLoaded = ! ! replies [ 0 ] ;
367
+ /**
368
+ * Loading animations:
369
+ * 0. Wait for the dialog to have animated in view and scaled up.
370
+ * 1. If replies have not loaded yet - show skeleton with 146px height,
371
+ * when the comments load, replace skeleton with replies and
372
+ * transition to height auto
373
+ * 2. If replies are already there - show replies with 0px height,
374
+ * transition to height: auto
375
+ *
376
+ */
377
+
378
+ const skeletonController = useAnimation ( ) ;
379
+ const repliesController = useAnimation ( ) ;
370
380
371
381
/** Wait another <delay>ms after the dialog has transitioned into view */
372
382
const delay = DIALOG_TRANSITION_DURATION + REPLY_TRANSITION_DELAY ;
373
- const REPLY_TRANSITION_DURATION = 0.25 ;
383
+ const REPLY_TRANSITION_DURATION = Math . max ( replies . length * 0.15 , 0.5 ) ;
384
+ const SKELETON_FADE_DURATION = 0.25 ;
374
385
const SKELETON_HEIGHT = 146 ;
375
386
387
+ // initial status of replies -
388
+ // this is false when it's the first time this specific comment is opened
389
+ // after that it will be true because we cache replies in state
390
+ const repliesAlreadyLoadedOnFirstRender = React . useRef ( ! ! replies [ 0 ] ) ;
391
+
392
+ // current status of replies-
393
+ const repliesLoaded = ! ! replies [ 0 ] ;
394
+
395
+ /** Welcome to the imperative world of timeline animations
396
+ *
397
+ * -------------------------------------------------------
398
+ * | | | | |
399
+ * t=0 t=R1 t=1 t=R2 t=2
400
+ *
401
+ * Legend:
402
+ * t=0 DOM has rendered, animations can be started
403
+ * t=1 Dialog's enter animation has completed, replies animations can start
404
+ * t=2 Replies animation have started
405
+ * t=R1 Replies have loaded before t=1
406
+ * t=R2 Replies have loaded after t=1
407
+ *
408
+ */
409
+
410
+ const [ T , setStepInTimeline ] = React . useState ( - 1 ) ;
411
+
412
+ /*
413
+ * T = 0 (DOM has rendered, animations can be started)
414
+ * If there are no replies, skip all of the animations
415
+ * If replies aren't loaded, show skeleton
416
+ * If replies are loaded, do nothing and wait for next animation
417
+ * */
418
+ React . useEffect ( ( ) => {
419
+ if ( ! replies . length ) {
420
+ // If the dialog is already open without any replies,
421
+ // just skip all of the animations for opening transitions
422
+ repliesController . set ( { opacity : 1 , height : 'auto' } ) ;
423
+ setStepInTimeline ( 2 ) ;
424
+ } else if ( ! repliesAlreadyLoadedOnFirstRender . current && T === - 1 ) {
425
+ skeletonController . set ( { height : SKELETON_HEIGHT , opacity : 1 } ) ;
426
+ setStepInTimeline ( 0 ) ;
427
+ }
428
+ } , [ skeletonController , repliesController , replies . length , T ] ) ;
429
+
430
+ /**
431
+ * T = 1 (Dialog's enter animation has completed, hence the delay)
432
+ * If replies have loaded, remove skeleton, transition the replies
433
+ * If replies have not loaded, do nothing and wait for next animation
434
+ */
435
+ React . useEffect ( ( ) => {
436
+ const timeout = window . setTimeout ( ( ) => {
437
+ if ( T >= 1 ) return ; // can't go back in time
438
+
439
+ if ( repliesLoaded ) {
440
+ skeletonController . set ( { position : 'absolute' } ) ;
441
+ skeletonController . start ( {
442
+ height : 0 ,
443
+ opacity : 0 ,
444
+ transition : { duration : SKELETON_FADE_DURATION } ,
445
+ } ) ;
446
+ repliesController . set ( { opacity : 1 } ) ;
447
+ repliesController . start ( {
448
+ height : 'auto' ,
449
+ transition : { duration : REPLY_TRANSITION_DURATION } ,
450
+ } ) ;
451
+
452
+ setStepInTimeline ( 2 ) ;
453
+ } else {
454
+ setStepInTimeline ( 1 ) ;
455
+ }
456
+ } , delay * 1000 ) ;
457
+ return ( ) => window . clearTimeout ( timeout ) ;
458
+ } , [
459
+ skeletonController ,
460
+ repliesController ,
461
+ repliesLoaded ,
462
+ delay ,
463
+ REPLY_TRANSITION_DURATION ,
464
+ T ,
465
+ ] ) ;
466
+
467
+ /**
468
+ * T = R1 or R2 (Replies have now loaded)
469
+ * this is a parralel async process and can happen before or after t=1
470
+ * If it's before T=1, do nothing, wait for T=1
471
+ * If it's after T=1, start replies transition now!
472
+ */
376
473
React . useEffect ( ( ) => {
377
- if ( repliesLoaded && ! isAnimating ) repliesRenderedCallback ( ) ;
378
- } , [ repliesLoaded , isAnimating , repliesRenderedCallback ] ) ;
474
+ if ( ! repliesLoaded ) {
475
+ // do nothing, wait for T=1
476
+ } else if ( T === 1 ) {
477
+ skeletonController . start ( {
478
+ height : 0 ,
479
+ opacity : 0 ,
480
+ transition : { duration : REPLY_TRANSITION_DURATION } ,
481
+ } ) ;
482
+ repliesController . set ( { opacity : 1 } ) ;
483
+ repliesController . start ( {
484
+ height : 'auto' ,
485
+ transition : { duration : REPLY_TRANSITION_DURATION } ,
486
+ } ) ;
487
+ setStepInTimeline ( 2 ) ;
488
+ }
489
+ } , [
490
+ T ,
491
+ repliesLoaded ,
492
+ REPLY_TRANSITION_DURATION ,
493
+ skeletonController ,
494
+ repliesController ,
495
+ ] ) ;
379
496
380
497
return (
381
- < motion . ul
382
- initial = { { height : repliesLoaded ? 0 : SKELETON_HEIGHT } }
383
- animate = { { height : 'auto' } }
384
- transition = { {
385
- delay,
386
- duration : REPLY_TRANSITION_DURATION ,
387
- } }
388
- style = { {
389
- minHeight : repliesLoaded ? 0 : SKELETON_HEIGHT ,
390
- overflow : 'visible' ,
391
- paddingLeft : 0 ,
392
- } }
393
- onAnimationComplete = { ( ) => setAnimating ( false ) }
394
- >
395
- { repliesLoaded ? (
498
+ < >
499
+ < motion . div
500
+ initial = { { height : 0 , opacity : 0 , overflow : 'hidden' } }
501
+ animate = { skeletonController }
502
+ >
503
+ < SkeletonReply />
504
+ </ motion . div >
505
+
506
+ < motion . ul
507
+ initial = { { height : 0 , opacity : 0 } }
508
+ animate = { repliesController }
509
+ style = { {
510
+ overflow : 'visible' ,
511
+ paddingLeft : 0 ,
512
+ margin : 0 ,
513
+ listStyle : 'none' ,
514
+ } }
515
+ >
396
516
< >
397
517
{ replies . map (
398
518
reply => reply && < Reply reply = { reply } key = { reply . id } />
399
519
) }
400
520
</ >
401
- ) : (
402
- < SkeletonReply />
403
- ) }
404
- </ motion . ul >
521
+ </ motion . ul >
522
+ </ >
405
523
) ;
406
524
} ;
407
525
0 commit comments