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.

233 lines
7.6 KiB

  1. import React from 'react';
  2. import PropTypes from 'prop-types';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import ImmutablePureComponent from 'react-immutable-pure-component';
  5. import StatusContent from 'flavours/glitch/components/status_content';
  6. import AttachmentList from 'flavours/glitch/components/attachment_list';
  7. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  8. import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
  9. import AvatarComposite from 'flavours/glitch/components/avatar_composite';
  10. import Permalink from 'flavours/glitch/components/permalink';
  11. import IconButton from 'flavours/glitch/components/icon_button';
  12. import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
  13. import { HotKeys } from 'react-hotkeys';
  14. import { autoPlayGif } from 'flavours/glitch/initial_state';
  15. import classNames from 'classnames';
  16. const messages = defineMessages({
  17. more: { id: 'status.more', defaultMessage: 'More' },
  18. open: { id: 'conversation.open', defaultMessage: 'View conversation' },
  19. reply: { id: 'status.reply', defaultMessage: 'Reply' },
  20. markAsRead: { id: 'conversation.mark_as_read', defaultMessage: 'Mark as read' },
  21. delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
  22. muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
  23. unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
  24. });
  25. class Conversation extends ImmutablePureComponent {
  26. static contextTypes = {
  27. router: PropTypes.object,
  28. };
  29. static propTypes = {
  30. conversationId: PropTypes.string.isRequired,
  31. accounts: ImmutablePropTypes.list.isRequired,
  32. lastStatus: ImmutablePropTypes.map,
  33. unread:PropTypes.bool.isRequired,
  34. scrollKey: PropTypes.string,
  35. onMoveUp: PropTypes.func,
  36. onMoveDown: PropTypes.func,
  37. markRead: PropTypes.func.isRequired,
  38. delete: PropTypes.func.isRequired,
  39. intl: PropTypes.object.isRequired,
  40. };
  41. state = {
  42. isExpanded: undefined,
  43. };
  44. parseClick = (e, destination) => {
  45. const { router } = this.context;
  46. const { lastStatus, unread, markRead } = this.props;
  47. if (!router) return;
  48. if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
  49. if (destination === undefined) {
  50. if (unread) {
  51. markRead();
  52. }
  53. destination = `/statuses/${lastStatus.get('id')}`;
  54. }
  55. let state = { ...router.history.location.state };
  56. state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
  57. router.history.push(destination, state);
  58. e.preventDefault();
  59. }
  60. };
  61. handleMouseEnter = ({ currentTarget }) => {
  62. if (autoPlayGif) {
  63. return;
  64. }
  65. const emojis = currentTarget.querySelectorAll('.custom-emoji');
  66. for (var i = 0; i < emojis.length; i++) {
  67. let emoji = emojis[i];
  68. emoji.src = emoji.getAttribute('data-original');
  69. }
  70. };
  71. handleMouseLeave = ({ currentTarget }) => {
  72. if (autoPlayGif) {
  73. return;
  74. }
  75. const emojis = currentTarget.querySelectorAll('.custom-emoji');
  76. for (var i = 0; i < emojis.length; i++) {
  77. let emoji = emojis[i];
  78. emoji.src = emoji.getAttribute('data-static');
  79. }
  80. };
  81. handleClick = () => {
  82. if (!this.context.router) {
  83. return;
  84. }
  85. const { lastStatus, unread, markRead } = this.props;
  86. if (unread) {
  87. markRead();
  88. }
  89. this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
  90. };
  91. handleMarkAsRead = () => {
  92. this.props.markRead();
  93. };
  94. handleReply = () => {
  95. this.props.reply(this.props.lastStatus, this.context.router.history);
  96. };
  97. handleDelete = () => {
  98. this.props.delete();
  99. };
  100. handleHotkeyMoveUp = () => {
  101. this.props.onMoveUp(this.props.conversationId);
  102. };
  103. handleHotkeyMoveDown = () => {
  104. this.props.onMoveDown(this.props.conversationId);
  105. };
  106. handleConversationMute = () => {
  107. this.props.onMute(this.props.lastStatus);
  108. };
  109. handleShowMore = () => {
  110. this.props.onToggleHidden(this.props.lastStatus);
  111. if (this.props.lastStatus.get('spoiler_text')) {
  112. this.setExpansion(!this.state.isExpanded);
  113. }
  114. };
  115. setExpansion = value => {
  116. this.setState({ isExpanded: value });
  117. };
  118. render () {
  119. const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
  120. if (lastStatus === null) {
  121. return null;
  122. }
  123. const isExpanded = this.props.settings.getIn(['content_warnings', 'shared_state']) ? !lastStatus.get('hidden') : this.state.isExpanded;
  124. const menu = [
  125. { text: intl.formatMessage(messages.open), action: this.handleClick },
  126. null,
  127. ];
  128. menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
  129. if (unread) {
  130. menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
  131. menu.push(null);
  132. }
  133. menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
  134. const names = accounts.map(a => <Permalink to={`/@${a.get('acct')}`} href={a.get('url')} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Permalink>).reduce((prev, cur) => [prev, ', ', cur]);
  135. const handlers = {
  136. reply: this.handleReply,
  137. open: this.handleClick,
  138. moveUp: this.handleHotkeyMoveUp,
  139. moveDown: this.handleHotkeyMoveDown,
  140. toggleHidden: this.handleShowMore,
  141. };
  142. let media = null;
  143. if (lastStatus.get('media_attachments').size > 0) {
  144. media = <AttachmentList compact media={lastStatus.get('media_attachments')} />;
  145. }
  146. return (
  147. <HotKeys handlers={handlers}>
  148. <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
  149. <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
  150. <AvatarComposite accounts={accounts} size={48} />
  151. </div>
  152. <div className='conversation__content'>
  153. <div className='conversation__content__info'>
  154. <div className='conversation__content__relative-time'>
  155. {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
  156. </div>
  157. <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
  158. <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
  159. </div>
  160. </div>
  161. <StatusContent
  162. status={lastStatus}
  163. parseClick={this.parseClick}
  164. expanded={isExpanded}
  165. onExpandedToggle={this.handleShowMore}
  166. collapsable
  167. media={media}
  168. />
  169. <div className='status__action-bar'>
  170. <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='comment' onClick={this.handleReply} />
  171. <div className='status__action-bar-dropdown'>
  172. <DropdownMenuContainer
  173. scrollKey={scrollKey}
  174. status={lastStatus}
  175. items={menu}
  176. icon='ellipsis-h'
  177. size={18}
  178. direction='right'
  179. title={intl.formatMessage(messages.more)}
  180. />
  181. </div>
  182. </div>
  183. </div>
  184. </div>
  185. </HotKeys>
  186. );
  187. }
  188. }
  189. export default injectIntl(Conversation);