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.

247 lines
6.6 KiB

8 years ago
8 years ago
8 years ago
  1. // Package imports //
  2. import React from 'react';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import escapeTextContentForBrowser from 'escape-html';
  5. import PropTypes from 'prop-types';
  6. import { FormattedMessage } from 'react-intl';
  7. import classnames from 'classnames';
  8. // Mastodon imports //
  9. import emojify from '../../../mastodon/emoji';
  10. import { isRtl } from '../../../mastodon/rtl';
  11. import Permalink from '../../../mastodon/components/permalink';
  12. export default class StatusContent extends React.PureComponent {
  13. static propTypes = {
  14. status: ImmutablePropTypes.map.isRequired,
  15. expanded: PropTypes.oneOf([true, false, null]),
  16. setExpansion: PropTypes.func,
  17. onHeightUpdate: PropTypes.func,
  18. media: PropTypes.element,
  19. mediaIcon: PropTypes.string,
  20. parseClick: PropTypes.func,
  21. disabled: PropTypes.bool,
  22. };
  23. state = {
  24. hidden: true,
  25. };
  26. componentDidMount () {
  27. const node = this.node;
  28. const links = node.querySelectorAll('a');
  29. for (var i = 0; i < links.length; ++i) {
  30. let link = links[i];
  31. let mention = this.props.status.get('mentions').find(item => link.href === item.get('url'));
  32. if (mention) {
  33. link.addEventListener('click', this.onMentionClick.bind(this, mention), false);
  34. link.setAttribute('title', mention.get('acct'));
  35. } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
  36. link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
  37. } else {
  38. link.addEventListener('click', this.onLinkClick.bind(this), false);
  39. link.setAttribute('title', link.href);
  40. }
  41. link.setAttribute('target', '_blank');
  42. link.setAttribute('rel', 'noopener');
  43. }
  44. }
  45. componentDidUpdate () {
  46. if (this.props.onHeightUpdate) {
  47. this.props.onHeightUpdate();
  48. }
  49. }
  50. onLinkClick = (e) => {
  51. if (this.props.expanded === false) {
  52. if (this.props.parseClick) this.props.parseClick(e);
  53. }
  54. }
  55. onMentionClick = (mention, e) => {
  56. if (this.props.parseClick) {
  57. this.props.parseClick(e, `/accounts/${mention.get('id')}`);
  58. }
  59. }
  60. onHashtagClick = (hashtag, e) => {
  61. hashtag = hashtag.replace(/^#/, '').toLowerCase();
  62. if (this.props.parseClick) {
  63. this.props.parseClick(e, `/timelines/tag/${hashtag}`);
  64. }
  65. }
  66. handleMouseDown = (e) => {
  67. this.startXY = [e.clientX, e.clientY];
  68. }
  69. handleMouseUp = (e) => {
  70. const { parseClick } = this.props;
  71. if (!this.startXY) {
  72. return;
  73. }
  74. const [ startX, startY ] = this.startXY;
  75. const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
  76. if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && (e.target.parentNode.localName === 'button' || e.target.parentNode.localName === 'a'))) {
  77. return;
  78. }
  79. if (deltaX + deltaY < 5 && e.button === 0 && parseClick) {
  80. parseClick(e);
  81. }
  82. this.startXY = null;
  83. }
  84. handleSpoilerClick = (e) => {
  85. e.preventDefault();
  86. if (this.props.setExpansion) {
  87. this.props.setExpansion(this.props.expanded ? null : true);
  88. } else {
  89. this.setState({ hidden: !this.state.hidden });
  90. }
  91. }
  92. setRef = (c) => {
  93. this.node = c;
  94. }
  95. render () {
  96. const {
  97. status,
  98. media,
  99. mediaIcon,
  100. parseClick,
  101. disabled,
  102. } = this.props;
  103. const hidden = (
  104. this.props.setExpansion ?
  105. !this.props.expanded :
  106. this.state.hidden
  107. );
  108. const content = { __html: emojify(status.get('content')) };
  109. const spoilerContent = {
  110. __html: emojify(escapeTextContentForBrowser(
  111. status.get('spoiler_text', '')
  112. )),
  113. };
  114. const directionStyle = { direction: 'ltr' };
  115. const classNames = classnames('status__content', {
  116. 'status__content--with-action': parseClick && !disabled,
  117. });
  118. if (isRtl(status.get('search_index'))) {
  119. directionStyle.direction = 'rtl';
  120. }
  121. if (status.get('spoiler_text').length > 0) {
  122. let mentionsPlaceholder = '';
  123. const mentionLinks = status.get('mentions').map(item => (
  124. <Permalink
  125. to={`/accounts/${item.get('id')}`}
  126. href={item.get('url')}
  127. key={item.get('id')}
  128. className='mention'
  129. >
  130. @<span>{item.get('username')}</span>
  131. </Permalink>
  132. )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
  133. const toggleText = hidden ? [
  134. <FormattedMessage
  135. id='status.show_more'
  136. defaultMessage='Show more'
  137. key='0'
  138. />,
  139. mediaIcon ? (
  140. <i
  141. className={
  142. `fa fa-fw fa-${mediaIcon} status__content__spoiler-icon`
  143. }
  144. aria-hidden='true'
  145. key='1'
  146. />
  147. ) : null,
  148. ] : [
  149. <FormattedMessage
  150. id='status.show_less'
  151. defaultMessage='Show less'
  152. key='0'
  153. />,
  154. ];
  155. if (hidden) {
  156. mentionsPlaceholder = <div>{mentionLinks}</div>;
  157. }
  158. return (
  159. <div className={classNames} ref={this.setRef}>
  160. <p
  161. style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}
  162. onMouseDown={this.handleMouseDown}
  163. onMouseUp={this.handleMouseUp}
  164. >
  165. <span dangerouslySetInnerHTML={spoilerContent} />
  166. {' '}
  167. <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>
  168. {toggleText}
  169. </button>
  170. </p>
  171. {mentionsPlaceholder}
  172. <div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
  173. <div
  174. style={directionStyle}
  175. onMouseDown={this.handleMouseDown}
  176. onMouseUp={this.handleMouseUp}
  177. dangerouslySetInnerHTML={content}
  178. />
  179. {media}
  180. </div>
  181. </div>
  182. );
  183. } else if (parseClick) {
  184. return (
  185. <div
  186. ref={this.setRef}
  187. className={classNames}
  188. style={directionStyle}
  189. >
  190. <div
  191. onMouseDown={this.handleMouseDown}
  192. onMouseUp={this.handleMouseUp}
  193. dangerouslySetInnerHTML={content}
  194. />
  195. {media}
  196. </div>
  197. );
  198. } else {
  199. return (
  200. <div
  201. ref={this.setRef}
  202. className='status__content'
  203. style={directionStyle}
  204. >
  205. <div dangerouslySetInnerHTML={content} />
  206. {media}
  207. </div>
  208. );
  209. }
  210. }
  211. }