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.

267 lines
9.5 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 { injectIntl, defineMessages, 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 Audio from '../../audio';
  14. import scheduleIdleTask from '../../ui/util/schedule_idle_task';
  15. import classNames from 'classnames';
  16. import Icon from 'mastodon/components/icon';
  17. import AnimatedNumber from 'mastodon/components/animated_number';
  18. const messages = defineMessages({
  19. public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
  20. unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
  21. private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
  22. direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
  23. });
  24. export default @injectIntl
  25. class DetailedStatus extends ImmutablePureComponent {
  26. static contextTypes = {
  27. router: PropTypes.object,
  28. };
  29. static propTypes = {
  30. status: ImmutablePropTypes.map,
  31. onOpenMedia: PropTypes.func.isRequired,
  32. onOpenVideo: PropTypes.func.isRequired,
  33. onToggleHidden: PropTypes.func.isRequired,
  34. measureHeight: PropTypes.bool,
  35. onHeightChange: PropTypes.func,
  36. domain: PropTypes.string.isRequired,
  37. compact: PropTypes.bool,
  38. showMedia: PropTypes.bool,
  39. onToggleMediaVisibility: PropTypes.func,
  40. };
  41. state = {
  42. height: null,
  43. };
  44. handleAccountClick = (e) => {
  45. if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.context.router) {
  46. e.preventDefault();
  47. this.context.router.history.push(`/accounts/${this.props.status.getIn(['account', 'id'])}`);
  48. }
  49. e.stopPropagation();
  50. }
  51. handleOpenVideo = (media, options) => {
  52. this.props.onOpenVideo(media, options);
  53. }
  54. handleExpandedToggle = () => {
  55. this.props.onToggleHidden(this.props.status);
  56. }
  57. _measureHeight (heightJustChanged) {
  58. if (this.props.measureHeight && this.node) {
  59. scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
  60. if (this.props.onHeightChange && heightJustChanged) {
  61. this.props.onHeightChange();
  62. }
  63. }
  64. }
  65. setRef = c => {
  66. this.node = c;
  67. this._measureHeight();
  68. }
  69. componentDidUpdate (prevProps, prevState) {
  70. this._measureHeight(prevState.height !== this.state.height);
  71. }
  72. handleModalLink = e => {
  73. e.preventDefault();
  74. let href;
  75. if (e.target.nodeName !== 'A') {
  76. href = e.target.parentNode.href;
  77. } else {
  78. href = e.target.href;
  79. }
  80. window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
  81. }
  82. render () {
  83. const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
  84. const outerStyle = { boxSizing: 'border-box' };
  85. const { intl, compact, deep } = this.props;
  86. if (!status) {
  87. return null;
  88. }
  89. let media = '';
  90. let applicationLink = '';
  91. let reblogLink = '';
  92. let reblogIcon = 'retweet';
  93. let favouriteLink = '';
  94. if (this.props.measureHeight) {
  95. outerStyle.height = `${this.state.height}px`;
  96. }
  97. if (status.get('media_attachments').size > 0) {
  98. if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
  99. const attachment = status.getIn(['media_attachments', 0]);
  100. media = (
  101. <Audio
  102. src={attachment.get('url')}
  103. alt={attachment.get('description')}
  104. duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
  105. poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
  106. backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
  107. foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
  108. accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
  109. height={150}
  110. />
  111. );
  112. } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
  113. const attachment = status.getIn(['media_attachments', 0]);
  114. media = (
  115. <Video
  116. preview={attachment.get('preview_url')}
  117. blurhash={attachment.get('blurhash')}
  118. src={attachment.get('url')}
  119. alt={attachment.get('description')}
  120. width={300}
  121. height={150}
  122. inline
  123. onOpenVideo={this.handleOpenVideo}
  124. sensitive={status.get('sensitive')}
  125. visible={this.props.showMedia}
  126. onToggleVisibility={this.props.onToggleMediaVisibility}
  127. />
  128. );
  129. } else {
  130. media = (
  131. <MediaGallery
  132. standalone
  133. sensitive={status.get('sensitive')}
  134. media={status.get('media_attachments')}
  135. height={300}
  136. onOpenMedia={this.props.onOpenMedia}
  137. visible={this.props.showMedia}
  138. onToggleVisibility={this.props.onToggleMediaVisibility}
  139. />
  140. );
  141. }
  142. } else if (status.get('spoiler_text').length === 0) {
  143. media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
  144. }
  145. if (status.get('application')) {
  146. applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>;
  147. }
  148. const visibilityIconInfo = {
  149. 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
  150. 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
  151. 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
  152. 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
  153. };
  154. const visibilityIcon = visibilityIconInfo[status.get('visibility')];
  155. const visibilityLink = <React.Fragment> · <Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></React.Fragment>;
  156. if (['private', 'direct'].includes(status.get('visibility'))) {
  157. reblogLink = '';
  158. } else if (this.context.router) {
  159. reblogLink = (
  160. <React.Fragment>
  161. <React.Fragment> · </React.Fragment>
  162. <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
  163. <Icon id={reblogIcon} />
  164. <span className='detailed-status__reblogs'>
  165. <AnimatedNumber value={status.get('reblogs_count')} />
  166. </span>
  167. </Link>
  168. </React.Fragment>
  169. );
  170. } else {
  171. reblogLink = (
  172. <React.Fragment>
  173. <React.Fragment> · </React.Fragment>
  174. <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
  175. <Icon id={reblogIcon} />
  176. <span className='detailed-status__reblogs'>
  177. <AnimatedNumber value={status.get('reblogs_count')} />
  178. </span>
  179. </a>
  180. </React.Fragment>
  181. );
  182. }
  183. if (this.context.router) {
  184. favouriteLink = (
  185. <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
  186. <Icon id='heart' />
  187. <span className='detailed-status__favorites'>
  188. <AnimatedNumber value={status.get('favourites_count')} />
  189. </span>
  190. </Link>
  191. );
  192. } else {
  193. favouriteLink = (
  194. <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
  195. <Icon id='heart' />
  196. <span className='detailed-status__favorites'>
  197. <AnimatedNumber value={status.get('favourites_count')} />
  198. </span>
  199. </a>
  200. );
  201. }
  202. let deepRec;
  203. if(deep != null) {
  204. deepRec = (
  205. <div className="detailed-status__button deep__number">
  206. <Icon id="tree" />
  207. <span>
  208. <FormattedNumber value={deep} />
  209. </span>
  210. </div>
  211. );
  212. }
  213. return (
  214. <div style={outerStyle}>
  215. <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
  216. <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
  217. <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
  218. <DisplayName account={status.get('account')} localDomain={this.props.domain} />
  219. </a>
  220. {deepRec}
  221. <StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
  222. {media}
  223. <div className='detailed-status__meta'>
  224. <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
  225. <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
  226. </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
  227. </div>
  228. </div>
  229. </div>
  230. );
  231. }
  232. }