export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
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) {
return {
type: STATUS_FETCH_REQUEST,
error,
};
};
+
+export function setStatusHeight (id, height) {
+ return {
+ type: STATUS_SET_HEIGHT,
+ id,
+ height,
+ };
+};
+
+export function clearStatusesHeight () {
+ return {
+ type: STATUSES_CLEAR_HEIGHT,
+ };
+};
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import escapeTextContentForBrowser from 'escape-html';
-import emojify from '../emoji';
export default class DisplayName extends React.PureComponent {
};
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: emojify(escapeTextContentForBrowser(displayName)) };
+ const displayNameHtml = { __html: this.props.account.get('display_name_html') };
return (
<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>
);
}
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import { FormattedMessage } from 'react-intl';
-import emojify from '../emoji';
-import escapeTextContentForBrowser from 'escape-html';
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';
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
+ onHeightChange: PropTypes.func,
me: PropTypes.number,
boostModal: PropTypes.bool,
autoPlayGif: PropTypes.bool,
state = {
isExpanded: false,
- isIntersecting: true, // assume intersecting until told otherwise
isHidden: false, // set to true in requestIdleCallback to trigger un-render
}
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) => {
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 (
- <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.get('content')}
</article>
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
- let displayName = status.getIn(['account', 'display_name']);
-
- if (displayName.length === 0) {
- displayName = status.getIn(['account', 'username']);
- }
-
- const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
+ 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__prepend'>
<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>
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import escapeTextContentForBrowser from 'escape-html';
import PropTypes from 'prop-types';
-import emojify from '../emoji';
import { isRtl } from '../rtl';
import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
- const content = { __html: emojify(status.get('content')) };
- const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
+ const content = { __html: status.get('contentHtml') };
+ const spoilerContent = { __html: status.get('spoilerHtml') };
const directionStyle = { direction: 'ltr' };
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
blockAccount,
muteAccount,
} from '../actions/accounts';
-import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
+import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
}
},
+ onHeightChange (status, height) {
+ dispatch(setStatusHeight(status.get('id'), height));
+ },
+
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
-import emojify from '../../../emoji';
-import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Motion from 'react-motion/lib/Motion';
return null;
}
- let displayName = account.get('display_name');
let info = '';
let actionBtn = '';
let lockedIcon = '';
- if (displayName.length === 0) {
- displayName = account.get('username');
- }
-
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>;
}
lockedIcon = <i className='fa fa-lock' />;
}
- const content = { __html: emojify(account.get('note')) };
- const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) };
+ const content = { __html: account.get('note_emojified') };
+ const displayNameHtml = { __html: account.get('display_name_html') };
return (
<div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
<div>
<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>
<div className='account__header__content' dangerouslySetInnerHTML={content} />
import Avatar from '../../../components/avatar';
import IconButton from '../../../components/icon_button';
import DisplayName from '../../../components/display_name';
-import emojify from '../../../emoji';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
return null;
}
- const content = { __html: emojify(status.get('content')) };
+ const content = { __html: status.get('contentHtml') };
return (
<div className='reply-indicator'>
import Permalink from '../../../components/permalink';
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
-import emojify from '../../../emoji';
import IconButton from '../../../components/icon_button';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
render () {
const { intl, account, onAuthorize, onReject } = this.props;
- const content = { __html: emojify(account.get('note')) };
+ const content = { __html: account.get('note_emojified') };
return (
<div className='account-authorize__wrapper'>
import AccountContainer from '../../../containers/account_container';
import { FormattedMessage } from 'react-intl';
import Permalink from '../../../components/permalink';
-import emojify from '../../../emoji';
-import escapeTextContentForBrowser from 'escape-html';
import ImmutablePureComponent from 'react-immutable-pure-component';
export default class Notification extends ImmutablePureComponent {
render () {
const { notification } = this.props;
const account = notification.get('account');
- const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username');
- 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 displayNameHtml = { __html: account.get('display_name_html') };
+ 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')) {
case 'follow':
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
-import emojify from '../../../emoji';
import Toggle from 'react-toggle';
export default class StatusCheckBox extends React.PureComponent {
render () {
const { status, checked, onToggle, disabled } = this.props;
- const content = { __html: emojify(status.get('content')) };
+ const content = { __html: status.get('contentHtml') };
if (status.get('reblog')) {
return null;
import { uploadCompose } from '../../actions/compose';
import { refreshHomeTimeline } from '../../actions/timelines';
import { refreshNotifications } from '../../actions/notifications';
+import { clearStatusesHeight } from '../../actions/statuses';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container';
};
handleResize = debounce(() => {
+ // The cached heights are no longer accurate, invalidate
+ this.props.dispatch(clearStatusesHeight());
+
this.setState({ width: window.innerWidth });
}, 500, {
trailing: true,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites';
import { STORE_HYDRATE } from '../actions/store';
+import emojify from '../emoji';
import { Map as ImmutableMap, fromJS } from 'immutable';
+import escapeTextContentForBrowser from 'escape-html';
const normalizeAccount = (state, account) => {
account = { ...account };
delete account.following_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));
};
CONTEXT_FETCH_SUCCESS,
STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS,
+ STATUS_SET_HEIGHT,
+ STATUSES_CLEAR_HEIGHT,
} from '../actions/statuses';
import {
TIMELINE_REFRESH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites';
import { SEARCH_FETCH_SUCCESS } from '../actions/search';
+import emojify from '../emoji';
import { Map as ImmutableMap, fromJS } from 'immutable';
+import escapeTextContentForBrowser from 'escape-html';
+
+const domParser = new DOMParser();
const normalizeStatus = (state, status) => {
if (!status) {
}
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;
};
+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();
export default function statuses(state = initialState, action) {
return deleteStatus(state, action.id, action.references);
case ACCOUNT_BLOCK_SUCCESS:
return filterStatuses(state, action.relationship);
+ case STATUS_SET_HEIGHT:
+ return setHeight(state, action.id, action.height);
+ case STATUSES_CLEAR_HEIGHT:
+ return clearHeights(state);
default:
return state;
}
overflow-y: scroll;
overflow-x: hidden;
flex: 1 1 auto;
- backface-visibility: hidden;
-webkit-overflow-scrolling: touch;
@supports(display: grid) { // hack to fix Chrome <57
contain: strict;
const account = fromJS({
username: 'bar',
acct: 'bar@baz',
- display_name: 'Foo',
+ display_name_html: '<p>Foo</p>',
});
const wrapper = render(<DisplayName account={account} />);
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');
- });
});