height: PropTypes.number.isRequired,
onOpenMedia: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
+ defaultWidth: PropTypes.number,
+ cacheWidth: PropTypes.func,
};
static defaultProps = {
state = {
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
+ width: this.props.defaultWidth,
};
componentWillReceiveProps (nextProps) {
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,
});
}
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;
state = {
fullscreen: null,
+ cachedMediaWidth: 250, // Default media/card width using default Mastodon theme
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
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) &&
}
}
+ cacheMediaWidth = (width) => {
+ if (width && this.state.cachedMediaWidth !== width) {
+ this.setState({ cachedMediaWidth: width });
+ }
+ }
+
componentWillUnmount () {
this.clearMouseIdleTimer();
this.detachScrollListener();
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>
))}
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
'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();
}
}
+ handleRef = c => {
+ this.node = c;
+ }
+
render () {
let media = null;
let statusAvatar, prepend, rebloggedByText;
if (hidden) {
return (
- <div>
+ <div ref={this.handleRef}>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
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>
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>
} 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>
);
}
onOpenMedia={this.props.onOpenMedia}
card={status.get('card')}
compact
+ cacheWidth={this.props.cacheMediaWidth}
+ defaultWidth={this.props.cachedMediaWidth}
/>
);
}
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')}>
onToggleHidden: PropTypes.func.isRequired,
status: PropTypes.option,
intl: PropTypes.object.isRequired,
+ getScrollPosition: PropTypes.func,
+ updateScrollBottom: PropTypes.func,
+ cacheMediaWidth: PropTypes.func,
+ cachedMediaWidth: PropTypes.number,
};
handleMoveUp = () => {
onMoveDown={this.handleMoveDown}
onMoveUp={this.handleMoveUp}
contextType='notifications'
+ getScrollPosition={this.props.getScrollPosition}
+ updateScrollBottom={this.props.updateScrollBottom}
+ cachedMediaWidth={this.props.cachedMediaWidth}
+ cacheMediaWidth={this.props.cacheMediaWidth}
/>
);
}
</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>
);
</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>
);
maxDescription: PropTypes.number,
onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.bool,
+ defaultWidth: PropTypes.number,
+ cacheWidth: PropTypes.func,
};
static defaultProps = {
};
state = {
- width: 280,
+ width: this.props.defaultWidth || 280,
embedded: false,
};
setRef = c => {
if (c) {
+ if (this.props.cacheWidth) this.props.cacheWidth(c.offsetWidth);
this.setState({ width: c.offsetWidth });
}
}
onCloseVideo: PropTypes.func,
detailed: PropTypes.bool,
inline: PropTypes.bool,
+ cacheWidth: PropTypes.func,
intl: PropTypes.object.isRequired,
};
volume: 0.5,
paused: true,
dragging: false,
- containerWidth: false,
+ containerWidth: this.props.width,
fullscreen: false,
hovered: false,
muted: false,
this.player = c;
if (c) {
+ if (this.props.cacheWidth) this.props.cacheWidth(this.player.offsetWidth);
this.setState({
containerWidth: c.offsetWidth,
});