The code powering m.abunchtell.com https://m.abunchtell.com
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.
 
 
 
 

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