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.

201 lines
6.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 '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 { Link } from 'react-router-dom';
  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. 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. handleMouseEnter = ({ currentTarget }) => {
  42. if (autoPlayGif) {
  43. return;
  44. }
  45. const emojis = currentTarget.querySelectorAll('.custom-emoji');
  46. for (var i = 0; i < emojis.length; i++) {
  47. let emoji = emojis[i];
  48. emoji.src = emoji.getAttribute('data-original');
  49. }
  50. };
  51. handleMouseLeave = ({ currentTarget }) => {
  52. if (autoPlayGif) {
  53. return;
  54. }
  55. const emojis = currentTarget.querySelectorAll('.custom-emoji');
  56. for (var i = 0; i < emojis.length; i++) {
  57. let emoji = emojis[i];
  58. emoji.src = emoji.getAttribute('data-static');
  59. }
  60. };
  61. handleClick = () => {
  62. if (!this.context.router) {
  63. return;
  64. }
  65. const { lastStatus, unread, markRead } = this.props;
  66. if (unread) {
  67. markRead();
  68. }
  69. this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
  70. };
  71. handleMarkAsRead = () => {
  72. this.props.markRead();
  73. };
  74. handleReply = () => {
  75. this.props.reply(this.props.lastStatus, this.context.router.history);
  76. };
  77. handleDelete = () => {
  78. this.props.delete();
  79. };
  80. handleHotkeyMoveUp = () => {
  81. this.props.onMoveUp(this.props.conversationId);
  82. };
  83. handleHotkeyMoveDown = () => {
  84. this.props.onMoveDown(this.props.conversationId);
  85. };
  86. handleConversationMute = () => {
  87. this.props.onMute(this.props.lastStatus);
  88. };
  89. handleShowMore = () => {
  90. this.props.onToggleHidden(this.props.lastStatus);
  91. };
  92. render () {
  93. const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
  94. if (lastStatus === null) {
  95. return null;
  96. }
  97. const menu = [
  98. { text: intl.formatMessage(messages.open), action: this.handleClick },
  99. null,
  100. ];
  101. menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
  102. if (unread) {
  103. menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
  104. menu.push(null);
  105. }
  106. menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
  107. const names = accounts.map(a => <Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Link>).reduce((prev, cur) => [prev, ', ', cur]);
  108. const handlers = {
  109. reply: this.handleReply,
  110. open: this.handleClick,
  111. moveUp: this.handleHotkeyMoveUp,
  112. moveDown: this.handleHotkeyMoveDown,
  113. toggleHidden: this.handleShowMore,
  114. };
  115. return (
  116. <HotKeys handlers={handlers}>
  117. <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
  118. <div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
  119. <AvatarComposite accounts={accounts} size={48} />
  120. </div>
  121. <div className='conversation__content'>
  122. <div className='conversation__content__info'>
  123. <div className='conversation__content__relative-time'>
  124. {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
  125. </div>
  126. <div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
  127. <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
  128. </div>
  129. </div>
  130. <StatusContent
  131. status={lastStatus}
  132. onClick={this.handleClick}
  133. expanded={!lastStatus.get('hidden')}
  134. onExpandedToggle={this.handleShowMore}
  135. collapsable
  136. />
  137. {lastStatus.get('media_attachments').size > 0 && (
  138. <AttachmentList
  139. compact
  140. media={lastStatus.get('media_attachments')}
  141. />
  142. )}
  143. <div className='status__action-bar'>
  144. <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' onClick={this.handleReply} />
  145. <div className='status__action-bar-dropdown'>
  146. <DropdownMenuContainer
  147. scrollKey={scrollKey}
  148. status={lastStatus}
  149. items={menu}
  150. icon='ellipsis-h'
  151. size={18}
  152. direction='right'
  153. title={intl.formatMessage(messages.more)}
  154. />
  155. </div>
  156. </div>
  157. </div>
  158. </div>
  159. </HotKeys>
  160. );
  161. }
  162. }
  163. export default injectIntl(Conversation);