@ -9,13 +9,11 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar' ;
import { FormattedMessage } from 'react-intl' ;
import ImmutablePureComponent from 'react-immutable-pure-component' ;
import scheduleIdleTask from '../features/ui/util/schedule_idle_task' ;
import { MediaGallery , VideoPlayer } from '../features/ui/util/async-components' ;
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle' ;
import getRectFromEntry from '../features/ui/util/get_rect_from_entry' ;
export default class Status extends ImmutablePureComponent {
@ -26,7 +24,6 @@ export default class Status extends ImmutablePureComponent {
static propTypes = {
status : ImmutablePropTypes . map ,
account : ImmutablePropTypes . map ,
wrapped : PropTypes . bool ,
onReply : PropTypes . func ,
onFavourite : PropTypes . func ,
onReblog : PropTypes . func ,
@ -40,14 +37,11 @@ export default class Status extends ImmutablePureComponent {
boostModal : PropTypes . bool ,
autoPlayGif : PropTypes . bool ,
muted : PropTypes . bool ,
intersectionObserverWrapper : PropTypes . object ,
index : PropTypes . oneOfType ( [ PropTypes . string , PropTypes . number ] ) ,
listLength : PropTypes . oneOfType ( [ PropTypes . string , PropTypes . number ] ) ,
hidden : PropTypes . bool ,
} ;
state = {
isExpanded : false ,
isHidden : false , // set to true in requestIdleCallback to trigger un-render
}
// Avoid checking props that are functions (and whose equality will always
@ -55,91 +49,15 @@ export default class Status extends ImmutablePureComponent {
updateOnProps = [
'status' ,
'account' ,
'wrapped' ,
'me' ,
'boostModal' ,
'autoPlayGif' ,
'muted' ,
'listLength ' ,
'hidden ' ,
]
updateOnStates = [ 'isExpanded' ]
shouldComponentUpdate ( nextProps , nextState ) {
if ( ! nextState . isIntersecting && nextState . isHidden ) {
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
// that either "isIntersecting" or "isHidden" matter, and then they're
// the only things that matter (and updated ARIA attributes).
return this . state . isIntersecting || ! this . state . isHidden || nextProps . listLength !== this . props . listLength ;
} else if ( nextState . isIntersecting && ! this . state . isIntersecting ) {
// If we're going from a non-intersecting state to an intersecting state,
// (i.e. offscreen to onscreen), then we definitely need to re-render
return true ;
}
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
return super . shouldComponentUpdate ( nextProps , nextState ) ;
}
componentDidMount ( ) {
if ( ! this . props . intersectionObserverWrapper ) {
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js
return ;
}
this . props . intersectionObserverWrapper . observe (
this . props . id ,
this . node ,
this . handleIntersection
) ;
this . componentMounted = true ;
}
componentWillUnmount ( ) {
if ( this . props . intersectionObserverWrapper ) {
this . props . intersectionObserverWrapper . unobserve ( this . props . id , this . node ) ;
}
this . componentMounted = false ;
}
handleIntersection = ( entry ) => {
if ( this . node && this . node . children . length !== 0 ) {
// save the height of the fully-rendered element
this . height = getRectFromEntry ( entry ) . height ;
if ( this . props . onHeightChange ) {
this . props . onHeightChange ( this . props . status , this . height ) ;
}
}
this . setState ( ( prevState ) => {
if ( prevState . isIntersecting && ! entry . isIntersecting ) {
scheduleIdleTask ( this . hideIfNotIntersecting ) ;
}
return {
isIntersecting : entry . isIntersecting ,
isHidden : false ,
} ;
} ) ;
}
hideIfNotIntersecting = ( ) => {
if ( ! this . componentMounted ) {
return ;
}
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
// See: https://github.com/tootsuite/mastodon/issues/2900
this . setState ( ( prevState ) => ( { isHidden : ! prevState . isIntersecting } ) ) ;
}
handleRef = ( node ) => {
this . node = node ;
}
handleClick = ( ) => {
if ( ! this . context . router ) {
return ;
@ -173,25 +91,19 @@ export default class Status extends ImmutablePureComponent {
let media = null ;
let statusAvatar ;
// Exclude intersectionObserverWrapper from `other` variable
// because intersection is managed in here.
const { status , account , intersectionObserverWrapper , index , listLength , wrapped , ... other } = this . props ;
const { isExpanded , isIntersecting , isHidden } = this . state ;
const { status , account , hidden , ... other } = this . props ;
const { isExpanded } = this . state ;
if ( status === null ) {
return null ;
}
const hasIntersectionObserverWrapper = ! ! this . props . intersectionObserverWrapper ;
const isHiddenForSure = isIntersecting === false && isHidden ;
const visibilityUnknownButHeightIsCached = isIntersecting === undefined && status . has ( 'height' ) ;
if ( hasIntersectionObserverWrapper && ( isHiddenForSure || visibilityUnknownButHeightIsCached ) ) {
if ( hidden ) {
return (
< article ref = { this . han dleRef } data - id = { status . get ( 'id' ) } aria - posinset = { index } aria - setsize = { listLength } tabIndex = '0' style = { { height : ` ${ this . height || status . get ( 'height' ) } px ` , opacity : 0 , o verflow : 'hidden' } } >
< div >
{ status . getIn ( [ 'account' , 'display_name' ] ) || status . getIn ( [ 'account' , 'username' ] ) }
{ status . get ( 'content' ) }
< / a r t i c l e >
< / d i v >
) ;
}
@ -199,14 +111,14 @@ export default class Status extends ImmutablePureComponent {
const display_name_html = { __html : status . getIn ( [ 'account' , 'display_name_html' ] ) } ;
return (
< article className = 'status__wrapper' ref = { this . handleRef } data- id = { status . get ( 'id' ) } aria - posinset = { index } aria - setsize = { listLength } tabIndex = '0' >
< div className = 'status__wrapper' data - id = { status . get ( 'id' ) } >
< div className = 'status__prepend' >
< div className = 'status__prepend-icon-wrapper' > < i className = 'fa fa-fw fa-retweet status__prepend-icon' / > < / d i v >
< FormattedMessage id = 'status.reblogged_by' defaultMessage = '{name} boosted' values = { { name : < a onClick = { this . handleAccountClick } data - id = { status . getIn ( [ 'account' , 'id' ] ) } href = { status . getIn ( [ 'account' , 'url' ] ) } className = 'status__display-name muted' > < strong dangerouslySetInnerHTML = { display_name_html } / > < /a> }} / >
< / d i v >
< Status { ... other } wrapped status= { status . get ( 'reblog' ) } account = { status . get ( 'account' ) } / >
< / a r t i c l e >
< Status { ... other } status = { status . get ( 'reblog' ) } account = { status . get ( 'account' ) } / >
< / d i v >
) ;
}
@ -235,7 +147,7 @@ export default class Status extends ImmutablePureComponent {
}
return (
< article aria - posinset = { index } aria - setsize = { listLength } className = { ` status ${ this . props . muted ? 'muted' : '' } status- ${ status . get ( 'visibility' ) } ` } data - id = { status . get ( 'id' ) } tabIndex = { wrapped ? null : '0' } ref = { this . handleRef } >
< div className = { ` status ${ this . props . muted ? 'muted' : '' } status- ${ status . get ( 'visibility' ) } ` } data - id = { status . get ( 'id' ) } >
< div className = 'status__info' >
< a href = { status . get ( 'url' ) } className = 'status__relative-time' target = '_blank' rel = 'noopener' > < RelativeTimestamp timestamp = { status . get ( 'created_at' ) } / > < / a >
@ -253,7 +165,7 @@ export default class Status extends ImmutablePureComponent {
{ media }
< StatusActionBar { ... this . props } / >
< / a r t i c l e >
< / d i v >
) ;
}