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.

271 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 } 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. <<<<<<< HEAD
  86. const { compact, deep } = this.props;
  87. =======
  88. const { intl, compact } = this.props;
  89. >>>>>>> master
  90. if (!status) {
  91. return null;
  92. }
  93. let media = '';
  94. let applicationLink = '';
  95. let reblogLink = '';
  96. let reblogIcon = 'retweet';
  97. let favouriteLink = '';
  98. if (this.props.measureHeight) {
  99. outerStyle.height = `${this.state.height}px`;
  100. }
  101. if (status.get('media_attachments').size > 0) {
  102. if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
  103. const attachment = status.getIn(['media_attachments', 0]);
  104. media = (
  105. <Audio
  106. src={attachment.get('url')}
  107. alt={attachment.get('description')}
  108. duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
  109. poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
  110. backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
  111. foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
  112. accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
  113. height={150}
  114. />
  115. );
  116. } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
  117. const attachment = status.getIn(['media_attachments', 0]);
  118. media = (
  119. <Video
  120. preview={attachment.get('preview_url')}
  121. blurhash={attachment.get('blurhash')}
  122. src={attachment.get('url')}
  123. alt={attachment.get('description')}
  124. width={300}
  125. height={150}
  126. inline
  127. onOpenVideo={this.handleOpenVideo}
  128. sensitive={status.get('sensitive')}
  129. visible={this.props.showMedia}
  130. onToggleVisibility={this.props.onToggleMediaVisibility}
  131. />
  132. );
  133. } else {
  134. media = (
  135. <MediaGallery
  136. standalone
  137. sensitive={status.get('sensitive')}
  138. media={status.get('media_attachments')}
  139. height={300}
  140. onOpenMedia={this.props.onOpenMedia}
  141. visible={this.props.showMedia}
  142. onToggleVisibility={this.props.onToggleMediaVisibility}
  143. />
  144. );
  145. }
  146. } else if (status.get('spoiler_text').length === 0) {
  147. media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
  148. }
  149. if (status.get('application')) {
  150. 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>;
  151. }
  152. const visibilityIconInfo = {
  153. 'public': { icon: 'globe', text: intl.formatMessage(messages.public_short) },
  154. 'unlisted': { icon: 'unlock', text: intl.formatMessage(messages.unlisted_short) },
  155. 'private': { icon: 'lock', text: intl.formatMessage(messages.private_short) },
  156. 'direct': { icon: 'envelope', text: intl.formatMessage(messages.direct_short) },
  157. };
  158. const visibilityIcon = visibilityIconInfo[status.get('visibility')];
  159. const visibilityLink = <React.Fragment> · <Icon id={visibilityIcon.icon} title={visibilityIcon.text} /></React.Fragment>;
  160. if (['private', 'direct'].includes(status.get('visibility'))) {
  161. reblogLink = '';
  162. } else if (this.context.router) {
  163. reblogLink = (
  164. <React.Fragment>
  165. <React.Fragment> · </React.Fragment>
  166. <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'>
  167. <Icon id={reblogIcon} />
  168. <span className='detailed-status__reblogs'>
  169. <AnimatedNumber value={status.get('reblogs_count')} />
  170. </span>
  171. </Link>
  172. </React.Fragment>
  173. );
  174. } else {
  175. reblogLink = (
  176. <React.Fragment>
  177. <React.Fragment> · </React.Fragment>
  178. <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
  179. <Icon id={reblogIcon} />
  180. <span className='detailed-status__reblogs'>
  181. <AnimatedNumber value={status.get('reblogs_count')} />
  182. </span>
  183. </a>
  184. </React.Fragment>
  185. );
  186. }
  187. if (this.context.router) {
  188. favouriteLink = (
  189. <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
  190. <Icon id='heart' />
  191. <span className='detailed-status__favorites'>
  192. <AnimatedNumber value={status.get('favourites_count')} />
  193. </span>
  194. </Link>
  195. );
  196. } else {
  197. favouriteLink = (
  198. <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
  199. <Icon id='heart' />
  200. <span className='detailed-status__favorites'>
  201. <AnimatedNumber value={status.get('favourites_count')} />
  202. </span>
  203. </a>
  204. );
  205. }
  206. let deepRec;
  207. if(deep != null) {
  208. deepRec = (
  209. <div className="detailed-status__button deep__number">
  210. <Icon id="tree" />
  211. <span>
  212. 【<FormattedNumber value={deep} />】
  213. </span>
  214. </div>
  215. );
  216. }
  217. return (
  218. <div style={outerStyle}>
  219. <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
  220. <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
  221. <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
  222. <DisplayName account={status.get('account')} localDomain={this.props.domain} />
  223. </a>
  224. {deepRec}
  225. <StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
  226. {media}
  227. <div className='detailed-status__meta'>
  228. <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
  229. <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
  230. </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
  231. </div>
  232. </div>
  233. </div>
  234. );
  235. }
  236. }