@@ -28,7 +28,7 @@ export const CommentDialog = props =>
2828
2929const DIALOG_WIDTH = 420 ;
3030const DIALOG_TRANSITION_DURATION = 0.25 ;
31- const REPLY_TRANSITION_DELAY = 0.25 ;
31+ const REPLY_TRANSITION_DELAY = 0.5 ;
3232
3333export const Dialog : React . FC = ( ) => {
3434 const { state } = useOvermind ( ) ;
@@ -135,12 +135,11 @@ export const Dialog: React.FC = () => {
135135 setEditing = { setEditing }
136136 hasReplies = { replies . length }
137137 />
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+ />
144143 </ Element >
145144 < AddReply
146145 comment = { comment }
@@ -365,43 +364,162 @@ const CommentBody = ({ comment, editing, setEditing, hasReplies }) => {
365364} ;
366365
367366const 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 ( ) ;
370380
371381 /** Wait another <delay>ms after the dialog has transitioned into view */
372382 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 ;
374385 const SKELETON_HEIGHT = 146 ;
375386
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+ */
376473 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+ ] ) ;
379496
380497 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+ >
396516 < >
397517 { replies . map (
398518 reply => reply && < Reply reply = { reply } key = { reply . id } />
399519 ) }
400520 </ >
401- ) : (
402- < SkeletonReply />
403- ) }
404- </ motion . ul >
521+ </ motion . ul >
522+ </ >
405523 ) ;
406524} ;
407525
0 commit comments