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.

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