Fix timeline jumps (#10001)
* Avoid two-step rendering of statuses as much as possible Cache width shared by Video player, MediaGallery and Cards at the ScrollableList level, pass it down through StatusList and Notifications. * Adjust scroll when new preview cards appear * Adjust scroll when statuses above the current scroll position are deleted
This commit is contained in:
		
							parent
							
								
									d9f0c7fb84
								
							
						
					
					
						commit
						e1dbdf7377
					
				
					 6 changed files with 134 additions and 12 deletions
				
			
		| 
						 | 
				
			
			@ -194,6 +194,8 @@ class MediaGallery extends React.PureComponent {
 | 
			
		|||
    height: PropTypes.number.isRequired,
 | 
			
		||||
    onOpenMedia: PropTypes.func.isRequired,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    defaultWidth: PropTypes.number,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
| 
						 | 
				
			
			@ -202,6 +204,7 @@ class MediaGallery extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  state = {
 | 
			
		||||
    visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
 | 
			
		||||
    width: this.props.defaultWidth,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  componentWillReceiveProps (nextProps) {
 | 
			
		||||
| 
						 | 
				
			
			@ -221,6 +224,7 @@ class MediaGallery extends React.PureComponent {
 | 
			
		|||
  handleRef = (node) => {
 | 
			
		||||
    if (node /*&& this.isStandaloneEligible()*/) {
 | 
			
		||||
      // offsetWidth triggers a layout, so only calculate when we need to
 | 
			
		||||
      if (this.props.cacheWidth) this.props.cacheWidth(node.offsetWidth);
 | 
			
		||||
      this.setState({
 | 
			
		||||
        width: node.offsetWidth,
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			@ -233,8 +237,10 @@ class MediaGallery extends React.PureComponent {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    const { media, intl, sensitive, height } = this.props;
 | 
			
		||||
    const { width, visible } = this.state;
 | 
			
		||||
    const { media, intl, sensitive, height, defaultWidth } = this.props;
 | 
			
		||||
    const { visible } = this.state;
 | 
			
		||||
 | 
			
		||||
    const width = this.state.width || defaultWidth;
 | 
			
		||||
 | 
			
		||||
    let children;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,7 @@ export default class ScrollableList extends PureComponent {
 | 
			
		|||
 | 
			
		||||
  state = {
 | 
			
		||||
    fullscreen: null,
 | 
			
		||||
    cachedMediaWidth: 250, // Default media/card width using default Mastodon theme
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  intersectionObserverWrapper = new IntersectionObserverWrapper();
 | 
			
		||||
| 
						 | 
				
			
			@ -130,6 +131,20 @@ export default class ScrollableList extends PureComponent {
 | 
			
		|||
    this.handleScroll();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getScrollPosition = () => {
 | 
			
		||||
    if (this.node && (this.node.scrollTop > 0 || this.mouseMovedRecently)) {
 | 
			
		||||
      return { height: this.node.scrollHeight, top: this.node.scrollTop };
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateScrollBottom = (snapshot) => {
 | 
			
		||||
    const newScrollTop = this.node.scrollHeight - snapshot;
 | 
			
		||||
 | 
			
		||||
    this.setScrollTop(newScrollTop);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getSnapshotBeforeUpdate (prevProps) {
 | 
			
		||||
    const someItemInserted = React.Children.count(prevProps.children) > 0 &&
 | 
			
		||||
      React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
 | 
			
		||||
| 
						 | 
				
			
			@ -150,6 +165,12 @@ export default class ScrollableList extends PureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  cacheMediaWidth = (width) => {
 | 
			
		||||
    if (width && this.state.cachedMediaWidth !== width) {
 | 
			
		||||
      this.setState({ cachedMediaWidth: width });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount () {
 | 
			
		||||
    this.clearMouseIdleTimer();
 | 
			
		||||
    this.detachScrollListener();
 | 
			
		||||
| 
						 | 
				
			
			@ -239,7 +260,12 @@ export default class ScrollableList extends PureComponent {
 | 
			
		|||
                intersectionObserverWrapper={this.intersectionObserverWrapper}
 | 
			
		||||
                saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
 | 
			
		||||
              >
 | 
			
		||||
                {child}
 | 
			
		||||
                {React.cloneElement(child, {
 | 
			
		||||
                  getScrollPosition: this.getScrollPosition,
 | 
			
		||||
                  updateScrollBottom: this.updateScrollBottom,
 | 
			
		||||
                  cachedMediaWidth: this.state.cachedMediaWidth,
 | 
			
		||||
                  cacheMediaWidth: this.cacheMediaWidth,
 | 
			
		||||
                })}
 | 
			
		||||
              </IntersectionObserverArticleContainer>
 | 
			
		||||
            ))}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,6 +68,10 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    onMoveUp: PropTypes.func,
 | 
			
		||||
    onMoveDown: PropTypes.func,
 | 
			
		||||
    showThread: PropTypes.bool,
 | 
			
		||||
    getScrollPosition: PropTypes.func,
 | 
			
		||||
    updateScrollBottom: PropTypes.func,
 | 
			
		||||
    cacheMediaWidth: PropTypes.func,
 | 
			
		||||
    cachedMediaWidth: PropTypes.number,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Avoid checking props that are functions (and whose equality will always
 | 
			
		||||
| 
						 | 
				
			
			@ -79,6 +83,43 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    'hidden',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  // Track height changes we know about to compensate scrolling
 | 
			
		||||
  componentDidMount () {
 | 
			
		||||
    this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status.get('card');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getSnapshotBeforeUpdate () {
 | 
			
		||||
    if (this.props.getScrollPosition) {
 | 
			
		||||
      return this.props.getScrollPosition();
 | 
			
		||||
    } else {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Compensate height changes
 | 
			
		||||
  componentDidUpdate (prevProps, prevState, snapshot) {
 | 
			
		||||
    const doShowCard  = !this.props.muted && !this.props.hidden && this.props.status.get('card');
 | 
			
		||||
    if (doShowCard && !this.didShowCard) {
 | 
			
		||||
      this.didShowCard = true;
 | 
			
		||||
      if (snapshot !== null && this.props.updateScrollBottom) {
 | 
			
		||||
        if (this.node && this.node.offsetTop < snapshot.top) {
 | 
			
		||||
          this.props.updateScrollBottom(snapshot.height - snapshot.top);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount() {
 | 
			
		||||
    if (this.node && this.props.getScrollPosition) {
 | 
			
		||||
      const position = this.props.getScrollPosition();
 | 
			
		||||
      if (position !== null && this.node.offsetTop < position.top) {
 | 
			
		||||
        requestAnimationFrame(() => {
 | 
			
		||||
          this.props.updateScrollBottom(position.height - position.top);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleClick = () => {
 | 
			
		||||
    if (this.props.onClick) {
 | 
			
		||||
      this.props.onClick();
 | 
			
		||||
| 
						 | 
				
			
			@ -165,6 +206,10 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleRef = c => {
 | 
			
		||||
    this.node = c;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render () {
 | 
			
		||||
    let media = null;
 | 
			
		||||
    let statusAvatar, prepend, rebloggedByText;
 | 
			
		||||
| 
						 | 
				
			
			@ -179,7 +224,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    if (hidden) {
 | 
			
		||||
      return (
 | 
			
		||||
        <div>
 | 
			
		||||
        <div ref={this.handleRef}>
 | 
			
		||||
          {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
 | 
			
		||||
          {status.get('content')}
 | 
			
		||||
        </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -194,7 +239,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
      return (
 | 
			
		||||
        <HotKeys handlers={minHandlers}>
 | 
			
		||||
          <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0'>
 | 
			
		||||
          <div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
 | 
			
		||||
            <FormattedMessage id='status.filtered' defaultMessage='Filtered' />
 | 
			
		||||
          </div>
 | 
			
		||||
        </HotKeys>
 | 
			
		||||
| 
						 | 
				
			
			@ -242,11 +287,12 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
                preview={video.get('preview_url')}
 | 
			
		||||
                src={video.get('url')}
 | 
			
		||||
                alt={video.get('description')}
 | 
			
		||||
                width={239}
 | 
			
		||||
                width={this.props.cachedMediaWidth}
 | 
			
		||||
                height={110}
 | 
			
		||||
                inline
 | 
			
		||||
                sensitive={status.get('sensitive')}
 | 
			
		||||
                onOpenVideo={this.handleOpenVideo}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
| 
						 | 
				
			
			@ -254,7 +300,16 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
      } else {
 | 
			
		||||
        media = (
 | 
			
		||||
          <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
 | 
			
		||||
            {Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={this.props.onOpenMedia} />}
 | 
			
		||||
            {Component => (
 | 
			
		||||
              <Component
 | 
			
		||||
                media={status.get('media_attachments')}
 | 
			
		||||
                sensitive={status.get('sensitive')}
 | 
			
		||||
                height={110}
 | 
			
		||||
                onOpenMedia={this.props.onOpenMedia}
 | 
			
		||||
                cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
                defaultWidth={this.props.cachedMediaWidth}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
          </Bundle>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -264,6 +319,8 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
          onOpenMedia={this.props.onOpenMedia}
 | 
			
		||||
          card={status.get('card')}
 | 
			
		||||
          compact
 | 
			
		||||
          cacheWidth={this.props.cacheMediaWidth}
 | 
			
		||||
          defaultWidth={this.props.cachedMediaWidth}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -290,7 +347,7 @@ class Status extends ImmutablePureComponent {
 | 
			
		|||
 | 
			
		||||
    return (
 | 
			
		||||
      <HotKeys handlers={handlers}>
 | 
			
		||||
        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}>
 | 
			
		||||
        <div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))} ref={this.handleRef}>
 | 
			
		||||
          {prepend}
 | 
			
		||||
 | 
			
		||||
          <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -34,6 +34,10 @@ class Notification extends ImmutablePureComponent {
 | 
			
		|||
    onToggleHidden: PropTypes.func.isRequired,
 | 
			
		||||
    status: PropTypes.option,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
    getScrollPosition: PropTypes.func,
 | 
			
		||||
    updateScrollBottom: PropTypes.func,
 | 
			
		||||
    cacheMediaWidth: PropTypes.func,
 | 
			
		||||
    cachedMediaWidth: PropTypes.number,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleMoveUp = () => {
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +132,10 @@ class Notification extends ImmutablePureComponent {
 | 
			
		|||
        onMoveDown={this.handleMoveDown}
 | 
			
		||||
        onMoveUp={this.handleMoveUp}
 | 
			
		||||
        contextType='notifications'
 | 
			
		||||
        getScrollPosition={this.props.getScrollPosition}
 | 
			
		||||
        updateScrollBottom={this.props.updateScrollBottom}
 | 
			
		||||
        cachedMediaWidth={this.props.cachedMediaWidth}
 | 
			
		||||
        cacheMediaWidth={this.props.cacheMediaWidth}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -148,7 +156,17 @@ class Notification extends ImmutablePureComponent {
 | 
			
		|||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} />
 | 
			
		||||
          <StatusContainer
 | 
			
		||||
            id={notification.get('status')}
 | 
			
		||||
            account={notification.get('account')}
 | 
			
		||||
            muted
 | 
			
		||||
            withDismiss
 | 
			
		||||
            hidden={!!this.props.hidden}
 | 
			
		||||
            getScrollPosition={this.props.getScrollPosition}
 | 
			
		||||
            updateScrollBottom={this.props.updateScrollBottom}
 | 
			
		||||
            cachedMediaWidth={this.props.cachedMediaWidth}
 | 
			
		||||
            cacheMediaWidth={this.props.cacheMediaWidth}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </HotKeys>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			@ -170,7 +188,17 @@ class Notification extends ImmutablePureComponent {
 | 
			
		|||
            </span>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} />
 | 
			
		||||
          <StatusContainer
 | 
			
		||||
            id={notification.get('status')}
 | 
			
		||||
            account={notification.get('account')}
 | 
			
		||||
            muted
 | 
			
		||||
            withDismiss
 | 
			
		||||
            hidden={this.props.hidden}
 | 
			
		||||
            getScrollPosition={this.props.getScrollPosition}
 | 
			
		||||
            updateScrollBottom={this.props.updateScrollBottom}
 | 
			
		||||
            cachedMediaWidth={this.props.cachedMediaWidth}
 | 
			
		||||
            cacheMediaWidth={this.props.cacheMediaWidth}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </HotKeys>
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,6 +60,8 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
    maxDescription: PropTypes.number,
 | 
			
		||||
    onOpenMedia: PropTypes.func.isRequired,
 | 
			
		||||
    compact: PropTypes.bool,
 | 
			
		||||
    defaultWidth: PropTypes.number,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static defaultProps = {
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +70,7 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  state = {
 | 
			
		||||
    width: 280,
 | 
			
		||||
    width: this.props.defaultWidth || 280,
 | 
			
		||||
    embedded: false,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -111,6 +113,7 @@ export default class Card extends React.PureComponent {
 | 
			
		|||
 | 
			
		||||
  setRef = c => {
 | 
			
		||||
    if (c) {
 | 
			
		||||
      if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
 | 
			
		||||
      this.setState({ width: c.offsetWidth });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -99,6 +99,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
    onCloseVideo: PropTypes.func,
 | 
			
		||||
    detailed: PropTypes.bool,
 | 
			
		||||
    inline: PropTypes.bool,
 | 
			
		||||
    cacheWidth: PropTypes.func,
 | 
			
		||||
    intl: PropTypes.object.isRequired,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -108,7 +109,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
    volume: 0.5,
 | 
			
		||||
    paused: true,
 | 
			
		||||
    dragging: false,
 | 
			
		||||
    containerWidth: false,
 | 
			
		||||
    containerWidth: this.props.width,
 | 
			
		||||
    fullscreen: false,
 | 
			
		||||
    hovered: false,
 | 
			
		||||
    muted: false,
 | 
			
		||||
| 
						 | 
				
			
			@ -128,6 +129,7 @@ class Video extends React.PureComponent {
 | 
			
		|||
    this.player = c;
 | 
			
		||||
 | 
			
		||||
    if (c) {
 | 
			
		||||
      if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth);
 | 
			
		||||
      this.setState({
 | 
			
		||||
        containerWidth: c.offsetWidth,
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue