1
0

index.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. import Immutable from 'immutable';
  2. import React from 'react';
  3. import { connect } from 'react-redux';
  4. import PropTypes from 'prop-types';
  5. import classNames from 'classnames';
  6. import ImmutablePropTypes from 'react-immutable-proptypes';
  7. import { createSelector } from 'reselect';
  8. import { fetchStatus } from '../../actions/statuses';
  9. import MissingIndicator from '../../components/missing_indicator';
  10. import LoadingIndicator from 'mastodon/components/loading_indicator';
  11. import DetailedStatus from './components/detailed_status';
  12. import ActionBar from './components/action_bar';
  13. import Column from '../ui/components/column';
  14. import {
  15. favourite,
  16. unfavourite,
  17. bookmark,
  18. unbookmark,
  19. reblog,
  20. unreblog,
  21. pin,
  22. unpin,
  23. } from '../../actions/interactions';
  24. import {
  25. replyCompose,
  26. mentionCompose,
  27. directCompose,
  28. } from '../../actions/compose';
  29. import {
  30. muteStatus,
  31. unmuteStatus,
  32. deleteStatus,
  33. editStatus,
  34. hideStatus,
  35. revealStatus,
  36. translateStatus,
  37. undoStatusTranslation,
  38. } from '../../actions/statuses';
  39. import {
  40. unblockAccount,
  41. unmuteAccount,
  42. } from '../../actions/accounts';
  43. import {
  44. blockDomain,
  45. unblockDomain,
  46. } from '../../actions/domain_blocks';
  47. import { initMuteModal } from '../../actions/mutes';
  48. import { initBlockModal } from '../../actions/blocks';
  49. import { initBoostModal } from '../../actions/boosts';
  50. import { initReport } from '../../actions/reports';
  51. import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
  52. import ScrollContainer from 'mastodon/containers/scroll_container';
  53. import ColumnBackButton from '../../components/column_back_button';
  54. import ColumnHeader from '../../components/column_header';
  55. import StatusContainer from '../../containers/status_container';
  56. import { openModal } from '../../actions/modal';
  57. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  58. import ImmutablePureComponent from 'react-immutable-pure-component';
  59. import { HotKeys } from 'react-hotkeys';
  60. import { boostModal, deleteModal } from '../../initial_state';
  61. import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
  62. import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
  63. import Icon from 'mastodon/components/icon';
  64. import { Helmet } from 'react-helmet';
  65. const messages = defineMessages({
  66. deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
  67. deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
  68. redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
  69. redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
  70. revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' },
  71. hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' },
  72. detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' },
  73. replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
  74. replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
  75. blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
  76. });
  77. const makeMapStateToProps = () => {
  78. const getStatus = makeGetStatus();
  79. const getPictureInPicture = makeGetPictureInPicture();
  80. const getAncestorsIds = createSelector([
  81. (_, { id }) => id,
  82. state => state.getIn(['contexts', 'inReplyTos']),
  83. ], (statusId, inReplyTos) => {
  84. let ancestorsIds = Immutable.List();
  85. ancestorsIds = ancestorsIds.withMutations(mutable => {
  86. let id = statusId;
  87. while (id && !mutable.includes(id)) {
  88. mutable.unshift(id);
  89. id = inReplyTos.get(id);
  90. }
  91. });
  92. return ancestorsIds;
  93. });
  94. const getDescendantsIds = createSelector([
  95. (_, { id }) => id,
  96. state => state.getIn(['contexts', 'replies']),
  97. state => state.get('statuses'),
  98. ], (statusId, contextReplies, statuses) => {
  99. let descendantsIds = [];
  100. const ids = [statusId];
  101. while (ids.length > 0) {
  102. let id = ids.pop();
  103. const replies = contextReplies.get(id);
  104. if (statusId !== id) {
  105. descendantsIds.push(id);
  106. }
  107. if (replies) {
  108. replies.reverse().forEach(reply => {
  109. if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
  110. });
  111. }
  112. }
  113. let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
  114. if (insertAt !== -1) {
  115. descendantsIds.forEach((id, idx) => {
  116. if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
  117. descendantsIds.splice(idx, 1);
  118. descendantsIds.splice(insertAt, 0, id);
  119. insertAt += 1;
  120. }
  121. });
  122. }
  123. return Immutable.List(descendantsIds);
  124. });
  125. const mapStateToProps = (state, props) => {
  126. const status = getStatus(state, { id: props.params.statusId });
  127. let ancestorsIds = Immutable.List();
  128. let descendantsIds = Immutable.List();
  129. if (status) {
  130. ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
  131. descendantsIds = getDescendantsIds(state, { id: status.get('id') });
  132. }
  133. return {
  134. isLoading: state.getIn(['statuses', props.params.statusId, 'isLoading']),
  135. status,
  136. ancestorsIds,
  137. descendantsIds,
  138. askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
  139. domain: state.getIn(['meta', 'domain']),
  140. pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
  141. };
  142. };
  143. return mapStateToProps;
  144. };
  145. const truncate = (str, num) => {
  146. if (str.length > num) {
  147. return str.slice(0, num) + '…';
  148. } else {
  149. return str;
  150. }
  151. };
  152. const titleFromStatus = status => {
  153. const displayName = status.getIn(['account', 'display_name']);
  154. const username = status.getIn(['account', 'username']);
  155. const prefix = displayName.trim().length === 0 ? username : displayName;
  156. const text = status.get('search_index');
  157. return `${prefix}: "${truncate(text, 30)}"`;
  158. };
  159. export default @injectIntl
  160. @connect(makeMapStateToProps)
  161. class Status extends ImmutablePureComponent {
  162. static contextTypes = {
  163. router: PropTypes.object,
  164. identity: PropTypes.object,
  165. };
  166. static propTypes = {
  167. params: PropTypes.object.isRequired,
  168. dispatch: PropTypes.func.isRequired,
  169. status: ImmutablePropTypes.map,
  170. isLoading: PropTypes.bool,
  171. ancestorsIds: ImmutablePropTypes.list,
  172. descendantsIds: ImmutablePropTypes.list,
  173. intl: PropTypes.object.isRequired,
  174. askReplyConfirmation: PropTypes.bool,
  175. multiColumn: PropTypes.bool,
  176. domain: PropTypes.string.isRequired,
  177. pictureInPicture: ImmutablePropTypes.contains({
  178. inUse: PropTypes.bool,
  179. available: PropTypes.bool,
  180. }),
  181. };
  182. state = {
  183. fullscreen: false,
  184. showMedia: defaultMediaVisibility(this.props.status),
  185. loadedStatusId: undefined,
  186. };
  187. componentWillMount () {
  188. this.props.dispatch(fetchStatus(this.props.params.statusId));
  189. }
  190. componentDidMount () {
  191. attachFullscreenListener(this.onFullScreenChange);
  192. }
  193. componentWillReceiveProps (nextProps) {
  194. if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) {
  195. this._scrolledIntoView = false;
  196. this.props.dispatch(fetchStatus(nextProps.params.statusId));
  197. }
  198. if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
  199. this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
  200. }
  201. }
  202. handleToggleMediaVisibility = () => {
  203. this.setState({ showMedia: !this.state.showMedia });
  204. }
  205. handleFavouriteClick = (status) => {
  206. const { dispatch } = this.props;
  207. const { signedIn } = this.context.identity;
  208. if (signedIn) {
  209. if (status.get('favourited')) {
  210. dispatch(unfavourite(status));
  211. } else {
  212. dispatch(favourite(status));
  213. }
  214. } else {
  215. dispatch(openModal('INTERACTION', {
  216. type: 'favourite',
  217. accountId: status.getIn(['account', 'id']),
  218. url: status.get('url'),
  219. }));
  220. }
  221. }
  222. handlePin = (status) => {
  223. if (status.get('pinned')) {
  224. this.props.dispatch(unpin(status));
  225. } else {
  226. this.props.dispatch(pin(status));
  227. }
  228. }
  229. handleReplyClick = (status) => {
  230. const { askReplyConfirmation, dispatch, intl } = this.props;
  231. const { signedIn } = this.context.identity;
  232. if (signedIn) {
  233. if (askReplyConfirmation) {
  234. dispatch(openModal('CONFIRM', {
  235. message: intl.formatMessage(messages.replyMessage),
  236. confirm: intl.formatMessage(messages.replyConfirm),
  237. onConfirm: () => dispatch(replyCompose(status, this.context.router.history)),
  238. }));
  239. } else {
  240. dispatch(replyCompose(status, this.context.router.history));
  241. }
  242. } else {
  243. dispatch(openModal('INTERACTION', {
  244. type: 'reply',
  245. accountId: status.getIn(['account', 'id']),
  246. url: status.get('url'),
  247. }));
  248. }
  249. }
  250. handleModalReblog = (status, privacy) => {
  251. this.props.dispatch(reblog(status, privacy));
  252. }
  253. handleReblogClick = (status, e) => {
  254. const { dispatch } = this.props;
  255. const { signedIn } = this.context.identity;
  256. if (signedIn) {
  257. if (status.get('reblogged')) {
  258. dispatch(unreblog(status));
  259. } else {
  260. if ((e && e.shiftKey) || !boostModal) {
  261. this.handleModalReblog(status);
  262. } else {
  263. dispatch(initBoostModal({ status, onReblog: this.handleModalReblog }));
  264. }
  265. }
  266. } else {
  267. dispatch(openModal('INTERACTION', {
  268. type: 'reblog',
  269. accountId: status.getIn(['account', 'id']),
  270. url: status.get('url'),
  271. }));
  272. }
  273. }
  274. handleBookmarkClick = (status) => {
  275. if (status.get('bookmarked')) {
  276. this.props.dispatch(unbookmark(status));
  277. } else {
  278. this.props.dispatch(bookmark(status));
  279. }
  280. }
  281. handleDeleteClick = (status, history, withRedraft = false) => {
  282. const { dispatch, intl } = this.props;
  283. if (!deleteModal) {
  284. dispatch(deleteStatus(status.get('id'), history, withRedraft));
  285. } else {
  286. dispatch(openModal('CONFIRM', {
  287. message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
  288. confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
  289. onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
  290. }));
  291. }
  292. }
  293. handleEditClick = (status, history) => {
  294. this.props.dispatch(editStatus(status.get('id'), history));
  295. }
  296. handleDirectClick = (account, router) => {
  297. this.props.dispatch(directCompose(account, router));
  298. }
  299. handleMentionClick = (account, router) => {
  300. this.props.dispatch(mentionCompose(account, router));
  301. }
  302. handleOpenMedia = (media, index) => {
  303. this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index }));
  304. }
  305. handleOpenVideo = (media, options) => {
  306. this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options }));
  307. }
  308. handleHotkeyOpenMedia = e => {
  309. const { status } = this.props;
  310. e.preventDefault();
  311. if (status.get('media_attachments').size > 0) {
  312. if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
  313. this.handleOpenVideo(status.getIn(['media_attachments', 0]), { startTime: 0 });
  314. } else {
  315. this.handleOpenMedia(status.get('media_attachments'), 0);
  316. }
  317. }
  318. }
  319. handleMuteClick = (account) => {
  320. this.props.dispatch(initMuteModal(account));
  321. }
  322. handleConversationMuteClick = (status) => {
  323. if (status.get('muted')) {
  324. this.props.dispatch(unmuteStatus(status.get('id')));
  325. } else {
  326. this.props.dispatch(muteStatus(status.get('id')));
  327. }
  328. }
  329. handleToggleHidden = (status) => {
  330. if (status.get('hidden')) {
  331. this.props.dispatch(revealStatus(status.get('id')));
  332. } else {
  333. this.props.dispatch(hideStatus(status.get('id')));
  334. }
  335. }
  336. handleToggleAll = () => {
  337. const { status, ancestorsIds, descendantsIds } = this.props;
  338. const statusIds = [status.get('id')].concat(ancestorsIds.toJS(), descendantsIds.toJS());
  339. if (status.get('hidden')) {
  340. this.props.dispatch(revealStatus(statusIds));
  341. } else {
  342. this.props.dispatch(hideStatus(statusIds));
  343. }
  344. }
  345. handleTranslate = status => {
  346. const { dispatch } = this.props;
  347. if (status.get('translation')) {
  348. dispatch(undoStatusTranslation(status.get('id')));
  349. } else {
  350. dispatch(translateStatus(status.get('id')));
  351. }
  352. }
  353. handleBlockClick = (status) => {
  354. const { dispatch } = this.props;
  355. const account = status.get('account');
  356. dispatch(initBlockModal(account));
  357. }
  358. handleReport = (status) => {
  359. this.props.dispatch(initReport(status.get('account'), status));
  360. }
  361. handleEmbed = (status) => {
  362. this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
  363. }
  364. handleUnmuteClick = account => {
  365. this.props.dispatch(unmuteAccount(account.get('id')));
  366. }
  367. handleUnblockClick = account => {
  368. this.props.dispatch(unblockAccount(account.get('id')));
  369. }
  370. handleBlockDomainClick = domain => {
  371. this.props.dispatch(openModal('CONFIRM', {
  372. message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,
  373. confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
  374. onConfirm: () => this.props.dispatch(blockDomain(domain)),
  375. }));
  376. }
  377. handleUnblockDomainClick = domain => {
  378. this.props.dispatch(unblockDomain(domain));
  379. }
  380. handleHotkeyMoveUp = () => {
  381. this.handleMoveUp(this.props.status.get('id'));
  382. }
  383. handleHotkeyMoveDown = () => {
  384. this.handleMoveDown(this.props.status.get('id'));
  385. }
  386. handleHotkeyReply = e => {
  387. e.preventDefault();
  388. this.handleReplyClick(this.props.status);
  389. }
  390. handleHotkeyFavourite = () => {
  391. this.handleFavouriteClick(this.props.status);
  392. }
  393. handleHotkeyBoost = () => {
  394. this.handleReblogClick(this.props.status);
  395. }
  396. handleHotkeyMention = e => {
  397. e.preventDefault();
  398. this.handleMentionClick(this.props.status.get('account'));
  399. }
  400. handleHotkeyOpenProfile = () => {
  401. this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
  402. }
  403. handleHotkeyToggleHidden = () => {
  404. this.handleToggleHidden(this.props.status);
  405. }
  406. handleHotkeyToggleSensitive = () => {
  407. this.handleToggleMediaVisibility();
  408. }
  409. handleMoveUp = id => {
  410. const { status, ancestorsIds, descendantsIds } = this.props;
  411. if (id === status.get('id')) {
  412. this._selectChild(ancestorsIds.size - 1, true);
  413. } else {
  414. let index = ancestorsIds.indexOf(id);
  415. if (index === -1) {
  416. index = descendantsIds.indexOf(id);
  417. this._selectChild(ancestorsIds.size + index, true);
  418. } else {
  419. this._selectChild(index - 1, true);
  420. }
  421. }
  422. }
  423. handleMoveDown = id => {
  424. const { status, ancestorsIds, descendantsIds } = this.props;
  425. if (id === status.get('id')) {
  426. this._selectChild(ancestorsIds.size + 1, false);
  427. } else {
  428. let index = ancestorsIds.indexOf(id);
  429. if (index === -1) {
  430. index = descendantsIds.indexOf(id);
  431. this._selectChild(ancestorsIds.size + index + 2, false);
  432. } else {
  433. this._selectChild(index + 1, false);
  434. }
  435. }
  436. }
  437. _selectChild (index, align_top) {
  438. const container = this.node;
  439. const element = container.querySelectorAll('.focusable')[index];
  440. if (element) {
  441. if (align_top && container.scrollTop > element.offsetTop) {
  442. element.scrollIntoView(true);
  443. } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
  444. element.scrollIntoView(false);
  445. }
  446. element.focus();
  447. }
  448. }
  449. renderChildren (list) {
  450. return list.map(id => (
  451. <StatusContainer
  452. key={id}
  453. id={id}
  454. onMoveUp={this.handleMoveUp}
  455. onMoveDown={this.handleMoveDown}
  456. contextType='thread'
  457. />
  458. ));
  459. }
  460. setRef = c => {
  461. this.node = c;
  462. }
  463. componentDidUpdate () {
  464. if (this._scrolledIntoView) {
  465. return;
  466. }
  467. const { status, ancestorsIds } = this.props;
  468. if (status && ancestorsIds && ancestorsIds.size > 0) {
  469. const element = this.node.querySelectorAll('.focusable')[ancestorsIds.size - 1];
  470. window.requestAnimationFrame(() => {
  471. element.scrollIntoView(true);
  472. });
  473. this._scrolledIntoView = true;
  474. }
  475. }
  476. componentWillUnmount () {
  477. detachFullscreenListener(this.onFullScreenChange);
  478. }
  479. onFullScreenChange = () => {
  480. this.setState({ fullscreen: isFullscreen() });
  481. }
  482. render () {
  483. let ancestors, descendants;
  484. const { isLoading, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
  485. const { fullscreen } = this.state;
  486. if (isLoading) {
  487. return (
  488. <Column>
  489. <LoadingIndicator />
  490. </Column>
  491. );
  492. }
  493. if (status === null) {
  494. return (
  495. <Column>
  496. <ColumnBackButton multiColumn={multiColumn} />
  497. <MissingIndicator />
  498. </Column>
  499. );
  500. }
  501. if (ancestorsIds && ancestorsIds.size > 0) {
  502. ancestors = <div>{this.renderChildren(ancestorsIds)}</div>;
  503. }
  504. if (descendantsIds && descendantsIds.size > 0) {
  505. descendants = <div>{this.renderChildren(descendantsIds)}</div>;
  506. }
  507. const isLocal = status.getIn(['account', 'acct'], '').indexOf('@') === -1;
  508. const isIndexable = !status.getIn(['account', 'noindex']);
  509. const handlers = {
  510. moveUp: this.handleHotkeyMoveUp,
  511. moveDown: this.handleHotkeyMoveDown,
  512. reply: this.handleHotkeyReply,
  513. favourite: this.handleHotkeyFavourite,
  514. boost: this.handleHotkeyBoost,
  515. mention: this.handleHotkeyMention,
  516. openProfile: this.handleHotkeyOpenProfile,
  517. toggleHidden: this.handleHotkeyToggleHidden,
  518. toggleSensitive: this.handleHotkeyToggleSensitive,
  519. openMedia: this.handleHotkeyOpenMedia,
  520. };
  521. return (
  522. <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}>
  523. <ColumnHeader
  524. showBackButton
  525. multiColumn={multiColumn}
  526. extraButton={(
  527. <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
  528. )}
  529. />
  530. <ScrollContainer scrollKey='thread'>
  531. <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
  532. {ancestors}
  533. <HotKeys handlers={handlers}>
  534. <div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}>
  535. <DetailedStatus
  536. key={`details-${status.get('id')}`}
  537. status={status}
  538. onOpenVideo={this.handleOpenVideo}
  539. onOpenMedia={this.handleOpenMedia}
  540. onToggleHidden={this.handleToggleHidden}
  541. onTranslate={this.handleTranslate}
  542. domain={domain}
  543. showMedia={this.state.showMedia}
  544. onToggleMediaVisibility={this.handleToggleMediaVisibility}
  545. pictureInPicture={pictureInPicture}
  546. />
  547. <ActionBar
  548. key={`action-bar-${status.get('id')}`}
  549. status={status}
  550. onReply={this.handleReplyClick}
  551. onFavourite={this.handleFavouriteClick}
  552. onReblog={this.handleReblogClick}
  553. onBookmark={this.handleBookmarkClick}
  554. onDelete={this.handleDeleteClick}
  555. onEdit={this.handleEditClick}
  556. onDirect={this.handleDirectClick}
  557. onMention={this.handleMentionClick}
  558. onMute={this.handleMuteClick}
  559. onUnmute={this.handleUnmuteClick}
  560. onMuteConversation={this.handleConversationMuteClick}
  561. onBlock={this.handleBlockClick}
  562. onUnblock={this.handleUnblockClick}
  563. onBlockDomain={this.handleBlockDomainClick}
  564. onUnblockDomain={this.handleUnblockDomainClick}
  565. onReport={this.handleReport}
  566. onPin={this.handlePin}
  567. onEmbed={this.handleEmbed}
  568. />
  569. </div>
  570. </HotKeys>
  571. {descendants}
  572. </div>
  573. </ScrollContainer>
  574. <Helmet>
  575. <title>{titleFromStatus(status)}</title>
  576. <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} />
  577. </Helmet>
  578. </Column>
  579. );
  580. }
  581. }