You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

222 lines
7.4 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import Avatar from '../../../components/avatar';
  5. import DisplayName from '../../../components/display_name';
  6. import StatusContent from '../../../components/status_content';
  7. import MediaGallery from '../../../components/media_gallery';
  8. import { Link } from 'react-router-dom';
  9. import { FormattedDate, FormattedNumber } from 'react-intl';
  10. import Card from './card';
  11. import ImmutablePureComponent from 'react-immutable-pure-component';
  12. import Video from '../../video';
  13. import scheduleIdleTask from '../../ui/util/schedule_idle_task';
  14. import classNames from 'classnames';
  15. import Icon from 'mastodon/components/icon';
  16. import PollContainer from 'mastodon/containers/poll_container';
  17. export default class DetailedStatus extends ImmutablePureComponent {
  18. static contextTypes = {
  19. router: PropTypes.object,
  20. };
  21. static propTypes = {
  22. status: ImmutablePropTypes.map,
  23. onOpenMedia: PropTypes.func.isRequired,
  24. onOpenVideo: PropTypes.func.isRequired,
  25. onToggleHidden: PropTypes.func.isRequired,
  26. measureHeight: PropTypes.bool,
  27. onHeightChange: PropTypes.func,
  28. domain: PropTypes.string.isRequired,
  29. compact: PropTypes.bool,
  30. showMedia: PropTypes.bool,
  31. onToggleMediaVisibility: PropTypes.func,
  32. };
  33. state = {
  34. height: null,
  35. };
  36. handleAccountClick = (e) => {
  37. if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
  38. e.preventDefault();
  39. this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
  40. }
  41. e.stopPropagation();
  42. }
  43. handleOpenVideo = (media, startTime) => {
  44. this.props.onOpenVideo(media, startTime);
  45. }
  46. handleExpandedToggle = () => {
  47. this.props.onToggleHidden(this.props.status);
  48. }
  49. _measureHeight (heightJustChanged) {
  50. if (this.props.measureHeight && this.node) {
  51. scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
  52. if (this.props.onHeightChange && heightJustChanged) {
  53. this.props.onHeightChange();
  54. }
  55. }
  56. }
  57. setRef = c => {
  58. this.node = c;
  59. this._measureHeight();
  60. }
  61. componentDidUpdate (prevProps, prevState) {
  62. this._measureHeight(prevState.height !== this.state.height);
  63. }
  64. handleModalLink = e => {
  65. e.preventDefault();
  66. let href;
  67. if (e.target.nodeName !== 'A') {
  68. href = e.target.parentNode.href;
  69. } else {
  70. href = e.target.href;
  71. }
  72. window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
  73. }
  74. render () {
  75. const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
  76. const outerStyle = { boxSizing: 'border-box' };
  77. const { compact } = this.props;
  78. if (!status) {
  79. return null;
  80. }
  81. let media = '';
  82. let applicationLink = '';
  83. let reblogLink = '';
  84. let reblogIcon = 'retweet';
  85. let favouriteLink = '';
  86. if (this.props.measureHeight) {
  87. outerStyle.height = `${this.state.height}px`;
  88. }
  89. if (status.get('poll')) {
  90. media = <PollContainer pollId={status.get('poll')} />;
  91. } else if (status.get('media_attachments').size > 0) {
  92. if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
  93. const video = status.getIn(['media_attachments', 0]);
  94. media = (
  95. <Video
  96. preview={video.get('preview_url')}
  97. blurhash={video.get('blurhash')}
  98. src={video.get('url')}
  99. alt={video.get('description')}
  100. width={300}
  101. height={150}
  102. inline
  103. onOpenVideo={this.handleOpenVideo}
  104. sensitive={status.get('sensitive')}
  105. visible={this.props.showMedia}
  106. onToggleVisibility={this.props.onToggleMediaVisibility}
  107. />
  108. );
  109. } else {
  110. media = (
  111. <MediaGallery
  112. standalone
  113. sensitive={status.get('sensitive')}
  114. media={status.get('media_attachments')}
  115. height={300}
  116. onOpenMedia={this.props.onOpenMedia}
  117. visible={this.props.showMedia}
  118. onToggleVisibility={this.props.onToggleMediaVisibility}
  119. />
  120. );
  121. }
  122. } else if (status.get('spoiler_text').length === 0) {
  123. media = <Card onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
  124. }
  125. if (status.get('application')) {
  126. applicationLink = <span> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener'>{status.getIn(['application', 'name'])}</a></span>;
  127. }
  128. if (status.get('visibility') === 'direct') {
  129. reblogIcon = 'envelope';
  130. } else if (status.get('visibility') === 'private') {
  131. reblogIcon = 'lock';
  132. }
  133. if (status.get('visibility') === 'private') {
  134. reblogLink = <Icon id={reblogIcon} />;
  135. } else if (this.context.router) {
  136. reblogLink = (
  137. <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
  138. <Icon id={reblogIcon} />
  139. <span className='detailed-status__reblogs'>
  140. <FormattedNumber value={status.get('reblogs_count')} />
  141. </span>
  142. </Link>
  143. );
  144. } else {
  145. reblogLink = (
  146. <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
  147. <Icon id={reblogIcon} />
  148. <span className='detailed-status__reblogs'>
  149. <FormattedNumber value={status.get('reblogs_count')} />
  150. </span>
  151. </a>
  152. );
  153. }
  154. if (this.context.router) {
  155. favouriteLink = (
  156. <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
  157. <Icon id='star' />
  158. <span className='detailed-status__favorites'>
  159. <FormattedNumber value={status.get('favourites_count')} />
  160. </span>
  161. </Link>
  162. );
  163. } else {
  164. favouriteLink = (
  165. <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
  166. <Icon id='star' />
  167. <span className='detailed-status__favorites'>
  168. <FormattedNumber value={status.get('favourites_count')} />
  169. </span>
  170. </a>
  171. );
  172. }
  173. return (
  174. <div style={outerStyle}>
  175. <div ref={this.setRef} className={classNames('detailed-status', { compact })}>
  176. <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
  177. <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
  178. <DisplayName account={status.get('account')} localDomain={this.props.domain} />
  179. </a>
  180. <StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
  181. {media}
  182. <div className='detailed-status__meta'>
  183. <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener'>
  184. <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
  185. </a>{applicationLink} · {reblogLink} · {favouriteLink}
  186. </div>
  187. </div>
  188. </div>
  189. );
  190. }
  191. }