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.

191 lines
7.0 KiB

  1. import React from 'react';
  2. import ImmutablePureComponent from 'react-immutable-pure-component';
  3. import ImmutablePropTypes from 'react-immutable-proptypes';
  4. import PropTypes from 'prop-types';
  5. import { connect } from 'react-redux';
  6. import { makeGetAccount } from 'mastodon/selectors';
  7. import Avatar from 'mastodon/components/avatar';
  8. import DisplayName from 'mastodon/components/display_name';
  9. import Permalink from 'mastodon/components/permalink';
  10. import RelativeTimestamp from 'mastodon/components/relative_timestamp';
  11. import IconButton from 'mastodon/components/icon_button';
  12. import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
  13. import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
  14. import { shortNumberFormat } from 'mastodon/utils/numbers';
  15. import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts';
  16. import { openModal } from 'mastodon/actions/modal';
  17. import { initMuteModal } from 'mastodon/actions/mutes';
  18. const messages = defineMessages({
  19. follow: { id: 'account.follow', defaultMessage: 'Follow' },
  20. unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
  21. requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
  22. unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
  23. unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
  24. unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
  25. });
  26. const makeMapStateToProps = () => {
  27. const getAccount = makeGetAccount();
  28. const mapStateToProps = (state, { id }) => ({
  29. account: getAccount(state, id),
  30. });
  31. return mapStateToProps;
  32. };
  33. const mapDispatchToProps = (dispatch, { intl }) => ({
  34. onFollow (account) {
  35. if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
  36. if (unfollowModal) {
  37. dispatch(openModal('CONFIRM', {
  38. message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
  39. confirm: intl.formatMessage(messages.unfollowConfirm),
  40. onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
  41. }));
  42. } else {
  43. dispatch(unfollowAccount(account.get('id')));
  44. }
  45. } else {
  46. dispatch(followAccount(account.get('id')));
  47. }
  48. },
  49. onBlock (account) {
  50. if (account.getIn(['relationship', 'blocking'])) {
  51. dispatch(unblockAccount(account.get('id')));
  52. } else {
  53. dispatch(blockAccount(account.get('id')));
  54. }
  55. },
  56. onMute (account) {
  57. if (account.getIn(['relationship', 'muting'])) {
  58. dispatch(unmuteAccount(account.get('id')));
  59. } else {
  60. dispatch(initMuteModal(account));
  61. }
  62. },
  63. });
  64. export default @injectIntl
  65. @connect(makeMapStateToProps, mapDispatchToProps)
  66. class AccountCard extends ImmutablePureComponent {
  67. static propTypes = {
  68. account: ImmutablePropTypes.map.isRequired,
  69. intl: PropTypes.object.isRequired,
  70. onFollow: PropTypes.func.isRequired,
  71. onBlock: PropTypes.func.isRequired,
  72. onMute: PropTypes.func.isRequired,
  73. };
  74. _updateEmojis () {
  75. const node = this.node;
  76. if (!node || autoPlayGif) {
  77. return;
  78. }
  79. const emojis = node.querySelectorAll('.custom-emoji');
  80. for (var i = 0; i < emojis.length; i++) {
  81. let emoji = emojis[i];
  82. if (emoji.classList.contains('status-emoji')) {
  83. continue;
  84. }
  85. emoji.classList.add('status-emoji');
  86. emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
  87. emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
  88. }
  89. }
  90. componentDidMount () {
  91. this._updateEmojis();
  92. }
  93. componentDidUpdate () {
  94. this._updateEmojis();
  95. }
  96. handleEmojiMouseEnter = ({ target }) => {
  97. target.src = target.getAttribute('data-original');
  98. }
  99. handleEmojiMouseLeave = ({ target }) => {
  100. target.src = target.getAttribute('data-static');
  101. }
  102. handleFollow = () => {
  103. this.props.onFollow(this.props.account);
  104. }
  105. handleBlock = () => {
  106. this.props.onBlock(this.props.account);
  107. }
  108. handleMute = () => {
  109. this.props.onMute(this.props.account);
  110. }
  111. setRef = (c) => {
  112. this.node = c;
  113. }
  114. render () {
  115. const { account, intl } = this.props;
  116. let buttons;
  117. if (account.get('id') !== me && account.get('relationship', null) !== null) {
  118. const following = account.getIn(['relationship', 'following']);
  119. const requested = account.getIn(['relationship', 'requested']);
  120. const blocking = account.getIn(['relationship', 'blocking']);
  121. const muting = account.getIn(['relationship', 'muting']);
  122. if (requested) {
  123. buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
  124. } else if (blocking) {
  125. buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
  126. } else if (muting) {
  127. buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
  128. } else if (!account.get('moved') || following) {
  129. buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
  130. }
  131. }
  132. return (
  133. <div className='directory__card'>
  134. <div className='directory__card__img'>
  135. <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' />
  136. </div>
  137. <div className='directory__card__bar'>
  138. <Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
  139. <Avatar account={account} size={48} />
  140. <DisplayName account={account} />
  141. </Permalink>
  142. <div className='directory__card__bar__relationship account__relationship'>
  143. {buttons}
  144. </div>
  145. </div>
  146. <div className='directory__card__extra' ref={this.setRef}>
  147. <div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
  148. </div>
  149. <div className='directory__card__extra'>
  150. <div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
  151. <div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
  152. <div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
  153. </div>
  154. </div>
  155. );
  156. }
  157. }