feat: Cache status height to avoid expensive renders (#4439)
* feat: Cache status height to avoid expensive renders * feat: Escape content and emojify in reducers * fix(css): Remove backface-visibility: hidden from .scrollable * fix(statuses): Avoid creating DOMParses inside a loop
This commit is contained in:
parent
199affb141
commit
ed7abec8f5
15 changed files with 83 additions and 57 deletions
|
@ -23,6 +23,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
|
||||||
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
|
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
|
||||||
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
|
||||||
|
|
||||||
|
export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT';
|
||||||
|
export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT';
|
||||||
|
|
||||||
export function fetchStatusRequest(id, skipLoading) {
|
export function fetchStatusRequest(id, skipLoading) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FETCH_REQUEST,
|
type: STATUS_FETCH_REQUEST,
|
||||||
|
@ -215,3 +218,17 @@ export function unmuteStatusFail(id, error) {
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function setStatusHeight (id, height) {
|
||||||
|
return {
|
||||||
|
type: STATUS_SET_HEIGHT,
|
||||||
|
id,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function clearStatusesHeight () {
|
||||||
|
return {
|
||||||
|
type: STATUSES_CLEAR_HEIGHT,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import emojify from '../emoji';
|
|
||||||
|
|
||||||
export default class DisplayName extends React.PureComponent {
|
export default class DisplayName extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -10,12 +8,11 @@ export default class DisplayName extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const displayName = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name');
|
const displayNameHtml = { __html: this.props.account.get('display_name_html') };
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className='display-name'>
|
<span className='display-name'>
|
||||||
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
|
<strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,6 @@ import DisplayName from './display_name';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import emojify from '../emoji';
|
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
||||||
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
|
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
|
||||||
|
@ -36,6 +34,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
onOpenMedia: PropTypes.func,
|
onOpenMedia: PropTypes.func,
|
||||||
onOpenVideo: PropTypes.func,
|
onOpenVideo: PropTypes.func,
|
||||||
onBlock: PropTypes.func,
|
onBlock: PropTypes.func,
|
||||||
|
onHeightChange: PropTypes.func,
|
||||||
me: PropTypes.number,
|
me: PropTypes.number,
|
||||||
boostModal: PropTypes.bool,
|
boostModal: PropTypes.bool,
|
||||||
autoPlayGif: PropTypes.bool,
|
autoPlayGif: PropTypes.bool,
|
||||||
|
@ -47,7 +46,6 @@ export default class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
isIntersecting: true, // assume intersecting until told otherwise
|
|
||||||
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +106,10 @@ export default class Status extends ImmutablePureComponent {
|
||||||
if (this.node && this.node.children.length !== 0) {
|
if (this.node && this.node.children.length !== 0) {
|
||||||
// save the height of the fully-rendered element
|
// save the height of the fully-rendered element
|
||||||
this.height = getRectFromEntry(entry).height;
|
this.height = getRectFromEntry(entry).height;
|
||||||
|
|
||||||
|
if (this.props.onHeightChange) {
|
||||||
|
this.props.onHeightChange(this.props.status, this.height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
|
@ -179,9 +181,13 @@ export default class Status extends ImmutablePureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isIntersecting && isHidden) {
|
const hasIntersectionObserverWrapper = !!this.props.intersectionObserverWrapper;
|
||||||
|
const isHiddenForSure = isIntersecting === false && isHidden;
|
||||||
|
const visibilityUnknownButHeightIsCached = isIntersecting === undefined && status.has('height');
|
||||||
|
|
||||||
|
if (hasIntersectionObserverWrapper && (isHiddenForSure || visibilityUnknownButHeightIsCached)) {
|
||||||
return (
|
return (
|
||||||
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}>
|
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height || status.get('height')}px`, opacity: 0, overflow: 'hidden' }}>
|
||||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||||
{status.get('content')}
|
{status.get('content')}
|
||||||
</article>
|
</article>
|
||||||
|
@ -189,19 +195,13 @@ export default class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
||||||
let displayName = status.getIn(['account', 'display_name']);
|
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
|
||||||
|
|
||||||
if (displayName.length === 0) {
|
|
||||||
displayName = status.getIn(['account', 'username']);
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
|
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
||||||
<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={displayNameHTML} /></a> }} />
|
<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> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
|
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import emojify from '../emoji';
|
|
||||||
import { isRtl } from '../rtl';
|
import { isRtl } from '../rtl';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Permalink from './permalink';
|
import Permalink from './permalink';
|
||||||
|
@ -119,8 +117,8 @@ export default class StatusContent extends React.PureComponent {
|
||||||
|
|
||||||
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
|
||||||
|
|
||||||
const content = { __html: emojify(status.get('content')) };
|
const content = { __html: status.get('contentHtml') };
|
||||||
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
|
const spoilerContent = { __html: status.get('spoilerHtml') };
|
||||||
const directionStyle = { direction: 'ltr' };
|
const directionStyle = { direction: 'ltr' };
|
||||||
const classNames = classnames('status__content', {
|
const classNames = classnames('status__content', {
|
||||||
'status__content--with-action': this.props.onClick && this.context.router,
|
'status__content--with-action': this.props.onClick && this.context.router,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
blockAccount,
|
blockAccount,
|
||||||
muteAccount,
|
muteAccount,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
|
import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses';
|
||||||
import { initReport } from '../actions/reports';
|
import { initReport } from '../actions/reports';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
@ -124,6 +124,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onHeightChange (status, height) {
|
||||||
|
dispatch(setStatusHeight(status.get('id'), height));
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import emojify from '../../../emoji';
|
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import Motion from 'react-motion/lib/Motion';
|
import Motion from 'react-motion/lib/Motion';
|
||||||
|
@ -92,15 +90,10 @@ export default class Header extends ImmutablePureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let displayName = account.get('display_name');
|
|
||||||
let info = '';
|
let info = '';
|
||||||
let actionBtn = '';
|
let actionBtn = '';
|
||||||
let lockedIcon = '';
|
let lockedIcon = '';
|
||||||
|
|
||||||
if (displayName.length === 0) {
|
|
||||||
displayName = account.get('username');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
||||||
info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
|
info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
|
||||||
}
|
}
|
||||||
|
@ -125,15 +118,15 @@ export default class Header extends ImmutablePureComponent {
|
||||||
lockedIcon = <i className='fa fa-lock' />;
|
lockedIcon = <i className='fa fa-lock' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = { __html: emojify(account.get('note')) };
|
const content = { __html: account.get('note_emojified') };
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
|
||||||
<div>
|
<div>
|
||||||
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
|
<Avatar account={account} autoPlayGif={this.props.autoPlayGif} />
|
||||||
|
|
||||||
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
|
<span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} />
|
||||||
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
|
<span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
|
||||||
<div className='account__header__content' dangerouslySetInnerHTML={content} />
|
<div className='account__header__content' dangerouslySetInnerHTML={content} />
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
import emojify from '../../../emoji';
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
|
@ -43,7 +42,7 @@ export default class ReplyIndicator extends ImmutablePureComponent {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = { __html: emojify(status.get('content')) };
|
const content = { __html: status.get('contentHtml') };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='reply-indicator'>
|
<div className='reply-indicator'>
|
||||||
|
|
|
@ -4,7 +4,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import Permalink from '../../../components/permalink';
|
import Permalink from '../../../components/permalink';
|
||||||
import Avatar from '../../../components/avatar';
|
import Avatar from '../../../components/avatar';
|
||||||
import DisplayName from '../../../components/display_name';
|
import DisplayName from '../../../components/display_name';
|
||||||
import emojify from '../../../emoji';
|
|
||||||
import IconButton from '../../../components/icon_button';
|
import IconButton from '../../../components/icon_button';
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
@ -26,7 +25,7 @@ export default class AccountAuthorize extends ImmutablePureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, account, onAuthorize, onReject } = this.props;
|
const { intl, account, onAuthorize, onReject } = this.props;
|
||||||
const content = { __html: emojify(account.get('note')) };
|
const content = { __html: account.get('note_emojified') };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='account-authorize__wrapper'>
|
<div className='account-authorize__wrapper'>
|
||||||
|
|
|
@ -4,8 +4,6 @@ import StatusContainer from '../../../containers/status_container';
|
||||||
import AccountContainer from '../../../containers/account_container';
|
import AccountContainer from '../../../containers/account_container';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import Permalink from '../../../components/permalink';
|
import Permalink from '../../../components/permalink';
|
||||||
import emojify from '../../../emoji';
|
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
export default class Notification extends ImmutablePureComponent {
|
export default class Notification extends ImmutablePureComponent {
|
||||||
|
@ -67,9 +65,8 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { notification } = this.props;
|
const { notification } = this.props;
|
||||||
const account = notification.get('account');
|
const account = notification.get('account');
|
||||||
const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
|
const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} />;
|
||||||
const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />;
|
|
||||||
|
|
||||||
switch(notification.get('type')) {
|
switch(notification.get('type')) {
|
||||||
case 'follow':
|
case 'follow':
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import emojify from '../../../emoji';
|
|
||||||
import Toggle from 'react-toggle';
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
export default class StatusCheckBox extends React.PureComponent {
|
export default class StatusCheckBox extends React.PureComponent {
|
||||||
|
@ -15,7 +14,7 @@ export default class StatusCheckBox extends React.PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, checked, onToggle, disabled } = this.props;
|
const { status, checked, onToggle, disabled } = this.props;
|
||||||
const content = { __html: emojify(status.get('content')) };
|
const content = { __html: status.get('contentHtml') };
|
||||||
|
|
||||||
if (status.get('reblog')) {
|
if (status.get('reblog')) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { debounce } from 'lodash';
|
||||||
import { uploadCompose } from '../../actions/compose';
|
import { uploadCompose } from '../../actions/compose';
|
||||||
import { refreshHomeTimeline } from '../../actions/timelines';
|
import { refreshHomeTimeline } from '../../actions/timelines';
|
||||||
import { refreshNotifications } from '../../actions/notifications';
|
import { refreshNotifications } from '../../actions/notifications';
|
||||||
|
import { clearStatusesHeight } from '../../actions/statuses';
|
||||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
import UploadArea from './components/upload_area';
|
import UploadArea from './components/upload_area';
|
||||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||||
|
@ -66,6 +67,9 @@ export default class UI extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
handleResize = debounce(() => {
|
handleResize = debounce(() => {
|
||||||
|
// The cached heights are no longer accurate, invalidate
|
||||||
|
this.props.dispatch(clearStatusesHeight());
|
||||||
|
|
||||||
this.setState({ width: window.innerWidth });
|
this.setState({ width: window.innerWidth });
|
||||||
}, 500, {
|
}, 500, {
|
||||||
trailing: true,
|
trailing: true,
|
||||||
|
|
|
@ -44,7 +44,9 @@ import {
|
||||||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
||||||
} from '../actions/favourites';
|
} from '../actions/favourites';
|
||||||
import { STORE_HYDRATE } from '../actions/store';
|
import { STORE_HYDRATE } from '../actions/store';
|
||||||
|
import emojify from '../emoji';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
const normalizeAccount = (state, account) => {
|
const normalizeAccount = (state, account) => {
|
||||||
account = { ...account };
|
account = { ...account };
|
||||||
|
@ -53,6 +55,10 @@ const normalizeAccount = (state, account) => {
|
||||||
delete account.following_count;
|
delete account.following_count;
|
||||||
delete account.statuses_count;
|
delete account.statuses_count;
|
||||||
|
|
||||||
|
const displayName = account.display_name.length === 0 ? account.username : account.display_name;
|
||||||
|
account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
|
||||||
|
account.note_emojified = emojify(account.note);
|
||||||
|
|
||||||
return state.set(account.id, fromJS(account));
|
return state.set(account.id, fromJS(account));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,8 @@ import {
|
||||||
CONTEXT_FETCH_SUCCESS,
|
CONTEXT_FETCH_SUCCESS,
|
||||||
STATUS_MUTE_SUCCESS,
|
STATUS_MUTE_SUCCESS,
|
||||||
STATUS_UNMUTE_SUCCESS,
|
STATUS_UNMUTE_SUCCESS,
|
||||||
|
STATUS_SET_HEIGHT,
|
||||||
|
STATUSES_CLEAR_HEIGHT,
|
||||||
} from '../actions/statuses';
|
} from '../actions/statuses';
|
||||||
import {
|
import {
|
||||||
TIMELINE_REFRESH_SUCCESS,
|
TIMELINE_REFRESH_SUCCESS,
|
||||||
|
@ -33,7 +35,11 @@ import {
|
||||||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
||||||
} from '../actions/favourites';
|
} from '../actions/favourites';
|
||||||
import { SEARCH_FETCH_SUCCESS } from '../actions/search';
|
import { SEARCH_FETCH_SUCCESS } from '../actions/search';
|
||||||
|
import emojify from '../emoji';
|
||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
|
||||||
|
const domParser = new DOMParser();
|
||||||
|
|
||||||
const normalizeStatus = (state, status) => {
|
const normalizeStatus = (state, status) => {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
|
@ -49,7 +55,9 @@ const normalizeStatus = (state, status) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n');
|
||||||
normalStatus.search_index = new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent;
|
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
|
||||||
|
normalStatus.contentHtml = emojify(normalStatus.content);
|
||||||
|
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''));
|
||||||
|
|
||||||
return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
|
return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus)));
|
||||||
};
|
};
|
||||||
|
@ -82,6 +90,18 @@ const filterStatuses = (state, relationship) => {
|
||||||
return state;
|
return state;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setHeight = (state, id, height) => {
|
||||||
|
return state.update(id, ImmutableMap(), map => map.set('height', height));
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearHeights = (state) => {
|
||||||
|
state.forEach(status => {
|
||||||
|
state = state.deleteIn([status.get('id'), 'height']);
|
||||||
|
});
|
||||||
|
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
const initialState = ImmutableMap();
|
const initialState = ImmutableMap();
|
||||||
|
|
||||||
export default function statuses(state = initialState, action) {
|
export default function statuses(state = initialState, action) {
|
||||||
|
@ -120,6 +140,10 @@ export default function statuses(state = initialState, action) {
|
||||||
return deleteStatus(state, action.id, action.references);
|
return deleteStatus(state, action.id, action.references);
|
||||||
case ACCOUNT_BLOCK_SUCCESS:
|
case ACCOUNT_BLOCK_SUCCESS:
|
||||||
return filterStatuses(state, action.relationship);
|
return filterStatuses(state, action.relationship);
|
||||||
|
case STATUS_SET_HEIGHT:
|
||||||
|
return setHeight(state, action.id, action.height);
|
||||||
|
case STATUSES_CLEAR_HEIGHT:
|
||||||
|
return clearHeights(state);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1572,7 +1572,6 @@
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
backface-visibility: hidden;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
@supports(display: grid) { // hack to fix Chrome <57
|
@supports(display: grid) { // hack to fix Chrome <57
|
||||||
contain: strict;
|
contain: strict;
|
||||||
|
|
|
@ -9,19 +9,9 @@ describe('<DisplayName />', () => {
|
||||||
const account = fromJS({
|
const account = fromJS({
|
||||||
username: 'bar',
|
username: 'bar',
|
||||||
acct: 'bar@baz',
|
acct: 'bar@baz',
|
||||||
display_name: 'Foo',
|
display_name_html: '<p>Foo</p>',
|
||||||
});
|
});
|
||||||
const wrapper = render(<DisplayName account={account} />);
|
const wrapper = render(<DisplayName account={account} />);
|
||||||
expect(wrapper).to.have.text('Foo @bar@baz');
|
expect(wrapper).to.have.text('Foo @bar@baz');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the username + account name if display name is empty', () => {
|
|
||||||
const account = fromJS({
|
|
||||||
username: 'bar',
|
|
||||||
acct: 'bar@baz',
|
|
||||||
display_name: '',
|
|
||||||
});
|
|
||||||
const wrapper = render(<DisplayName account={account} />);
|
|
||||||
expect(wrapper).to.have.text('bar @bar@baz');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue