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
6.9 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 'mastodon/components/status_content';
  6. import AttachmentList from 'mastodon/components/attachment_list';
  7. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  8. import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
  9. import AvatarComposite from 'mastodon/components/avatar_composite';
  10. import Permalink from 'mastodon/components/permalink';
  11. import IconButton from 'mastodon/components/icon_button';
  12. import RelativeTimestamp from 'mastodon/components/relative_timestamp';
  13. import { HotKeys } from 'react-hotkeys';
  14. import { autoPlayGif } from 'mastodon/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. export default @injectIntl
  26. class Conversation extends ImmutablePureComponent {
  27. static contextTypes = {
  28. router: PropTypes.object,
  29. };
  30. static propTypes = {
  31. conversationId: PropTypes.string.isRequired,
  32. accounts: ImmutablePropTypes.list.isRequired,
  33. lastStatus: ImmutablePropTypes.map,
  34. unread:PropTypes.bool.isRequired,
  35. scrollKey: PropTypes.string,
  36. onMoveUp: PropTypes.func,
  37. onMoveDown: PropTypes.func,
  38. markRead: PropTypes.func.isRequired,
  39. delete: PropTypes.func.isRequired,
  40. intl: PropTypes.object.isRequired,
  41. };
  42. _updateEmojis () {
  43. const node = this.namesNode;
  44. if (!node || autoPlayGif) {
  45. return;
  46. }
  47. const emojis = node.querySelectorAll('.custom-emoji');
  48. for (var i = 0; i < emojis.length; i++) {
  49. let emoji = emojis[i];
  50. if (emoji.classList.contains('status-emoji')) {
  51. continue;
  52. }
  53. emoji.classList.add('status-emoji');
  54. emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
  55. emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
  56. }
  57. }
  58. componentDidMount () {
  59. this._updateEmojis();
  60. }
  61. componentDidUpdate () {
  62. this._updateEmojis();
  63. }
  64. handleEmojiMouseEnter = ({ target }) => {
  65. target.src = target.getAttribute('data-original');
  66. }
  67. handleEmojiMouseLeave = ({ target }) => {
  68. target.src = target.getAttribute('data-static');
  69. }
  70. handleClick = () => {
  71. if (!this.context.router) {
  72. return;
  73. }
  74. const { lastStatus, unread, markRead } = this.props;
  75. if (unread) {
  76. markRead();
  77. }
  78. this.context.router.history.push(`/statuses/${lastStatus.get('id')}`);
  79. }
  80. handleMarkAsRead = () => {
  81. this.props.markRead();
  82. }
  83. handleReply = () => {
  84. this.props.reply(this.props.lastStatus, this.context.router.history);
  85. }
  86. handleDelete = () => {
  87. this.props.delete();
  88. }
  89. handleHotkeyMoveUp = () => {
  90. this.props.onMoveUp(this.props.conversationId);
  91. }
  92. handleHotkeyMoveDown = () => {
  93. this.props.onMoveDown(this.props.conversationId);
  94. }
  95. handleConversationMute = () => {
  96. this.props.onMute(this.props.lastStatus);
  97. }
  98. handleShowMore = () => {
  99. this.props.onToggleHidden(this.props.lastStatus);
  100. }
  101. setNamesRef = (c) => {
  102. this.namesNode = c;
  103. }
  104. render () {
  105. const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
  106. if (lastStatus === null) {
  107. return null;
  108. }
  109. const menu = [
  110. { text: intl.formatMessage(messages.open), action: this.handleClick },
  111. null,
  112. ];
  113. menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
  114. if (unread) {
  115. menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
  116. menu.push(null);
  117. }
  118. menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
  119. const names = accounts.map(a => <Permalink to={`/accounts/${a.get('id')}`} 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]);
  120. const handlers = {
  121. reply: this.handleReply,
  122. open: this.handleClick,
  123. moveUp: this.handleHotkeyMoveUp,
  124. moveDown: this.handleHotkeyMoveDown,
  125. toggleHidden: this.handleShowMore,
  126. };
  127. return (
  128. <HotKeys handlers={handlers}>
  129. <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
  130. <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
  131. <AvatarComposite accounts={accounts} size={48} />
  132. </div>
  133. <div className='conversation__content'>
  134. <div className='conversation__content__info'>
  135. <div className='conversation__content__relative-time'>
  136. {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
  137. </div>
  138. <div className='conversation__content__names' ref={this.setNamesRef}>
  139. <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
  140. </div>
  141. </div>
  142. <StatusContent
  143. status={lastStatus}
  144. onClick={this.handleClick}
  145. expanded={!lastStatus.get('hidden')}
  146. onExpandedToggle={this.handleShowMore}
  147. collapsable
  148. />
  149. {lastStatus.get('media_attachments').size > 0 && (
  150. <AttachmentList
  151. compact
  152. media={lastStatus.get('media_attachments')}
  153. />
  154. )}
  155. <div className='status__action-bar'>
  156. <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
  157. <div className='status__action-bar-dropdown'>
  158. <DropdownMenuContainer
  159. scrollKey={scrollKey}
  160. status={lastStatus}
  161. items={menu}
  162. icon='ellipsis-h'
  163. size={18}
  164. direction='right'
  165. title={intl.formatMessage(messages.more)}
  166. />
  167. </div>
  168. </div>
  169. </div>
  170. </div>
  171. </HotKeys>
  172. );
  173. }
  174. }