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.

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