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.

241 lines
6.6 KiB

  1. /*
  2. `<AccountHeader>`
  3. =================
  4. > For more information on the contents of this file, please contact:
  5. >
  6. > - kibigo! [@kibi@glitch.social]
  7. Original file by @gargron@mastodon.social et al as part of
  8. tootsuite/mastodon. We've expanded it in order to handle user bio
  9. frontmatter.
  10. The `<AccountHeader>` component provides the header for account
  11. timelines. It is a fairly simple component which mostly just consists
  12. of a `render()` method.
  13. __Props:__
  14. - __`account` (`ImmutablePropTypes.map`) :__
  15. The account to render a header for.
  16. - __`me` (`PropTypes.number.isRequired`) :__
  17. The id of the currently-signed-in account.
  18. - __`onFollow` (`PropTypes.func.isRequired`) :__
  19. The function to call when the user clicks the "follow" button.
  20. - __`intl` (`PropTypes.object.isRequired`) :__
  21. Our internationalization object, inserted by `@injectIntl`.
  22. */
  23. // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  24. /*
  25. Imports:
  26. --------
  27. */
  28. // Package imports //
  29. import React from 'react';
  30. import ImmutablePropTypes from 'react-immutable-proptypes';
  31. import PropTypes from 'prop-types';
  32. import escapeTextContentForBrowser from 'escape-html';
  33. import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
  34. import ImmutablePureComponent from 'react-immutable-pure-component';
  35. // Mastodon imports //
  36. import emojify from '../../../mastodon/emoji';
  37. import IconButton from '../../../mastodon/components/icon_button';
  38. import Avatar from '../../../mastodon/components/avatar';
  39. // Our imports //
  40. import { processBio } from '../../util/bio_metadata';
  41. // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  42. /*
  43. Inital setup:
  44. -------------
  45. The `messages` constant is used to define any messages that we need
  46. from inside props. In our case, these are the `unfollow`, `follow`, and
  47. `requested` messages used in the `title` of our buttons.
  48. */
  49. const messages = defineMessages({
  50. unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
  51. follow: { id: 'account.follow', defaultMessage: 'Follow' },
  52. requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
  53. });
  54. // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  55. /*
  56. Implementation:
  57. ---------------
  58. */
  59. @injectIntl
  60. export default class AccountHeader extends ImmutablePureComponent {
  61. static propTypes = {
  62. account : ImmutablePropTypes.map,
  63. me : PropTypes.number.isRequired,
  64. onFollow : PropTypes.func.isRequired,
  65. intl : PropTypes.object.isRequired,
  66. };
  67. /*
  68. ### `render()`
  69. The `render()` function is used to render our component.
  70. */
  71. render () {
  72. const { account, me, intl } = this.props;
  73. /*
  74. If no `account` is provided, then we can't render a header. Otherwise,
  75. we get the `displayName` for the account, if available. If it's blank,
  76. then we set the `displayName` to just be the `username` of the account.
  77. */
  78. if (!account) {
  79. return null;
  80. }
  81. let displayName = account.get('display_name');
  82. let info = '';
  83. let actionBtn = '';
  84. let following = false;
  85. if (displayName.length === 0) {
  86. displayName = account.get('username');
  87. }
  88. /*
  89. Next, we handle the account relationships. If the account follows the
  90. user, then we add an `info` message. If the user has requested a
  91. follow, then we disable the `actionBtn` and display an hourglass.
  92. Otherwise, if the account isn't blocked, we set the `actionBtn` to the
  93. appropriate icon.
  94. */
  95. if (me !== account.get('id')) {
  96. if (account.getIn(['relationship', 'followed_by'])) {
  97. info = (
  98. <span className='account--follows-info'>
  99. <FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
  100. </span>
  101. );
  102. }
  103. if (account.getIn(['relationship', 'requested'])) {
  104. actionBtn = (
  105. <div className='account--action-button'>
  106. <IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
  107. </div>
  108. );
  109. } else if (!account.getIn(['relationship', 'blocking'])) {
  110. following = account.getIn(['relationship', 'following']);
  111. actionBtn = (
  112. <div className='account--action-button'>
  113. <IconButton
  114. size={26}
  115. icon={following ? 'user-times' : 'user-plus'}
  116. active={following}
  117. title={intl.formatMessage(following ? messages.unfollow : messages.follow)}
  118. onClick={this.props.onFollow}
  119. />
  120. </div>
  121. );
  122. }
  123. }
  124. /*
  125. `displayNameHTML` processes the `displayName` and prepares it for
  126. insertion into the document. Meanwhile, we extract the `text` and
  127. `metadata` from our account's `note` using `processBio()`.
  128. */
  129. const displayNameHTML = {
  130. __html : emojify(escapeTextContentForBrowser(displayName)),
  131. };
  132. const { text, metadata } = processBio(account.get('note'));
  133. /*
  134. Here, we render our component using all the things we've defined above.
  135. */
  136. return (
  137. <div className='account__header__wrapper'>
  138. <div
  139. className='account__header'
  140. style={{ backgroundImage: `url(${account.get('header')})` }}
  141. >
  142. <div>
  143. <a href={account.get('url')} target='_blank' rel='noopener'>
  144. <span className='account__header__avatar'>
  145. <Avatar
  146. src={account.get('avatar')}
  147. staticSrc={account.get('avatar_static')}
  148. size={90}
  149. />
  150. </span>
  151. <span
  152. className='account__header__display-name'
  153. dangerouslySetInnerHTML={displayNameHTML}
  154. />
  155. </a>
  156. <span className='account__header__username'>
  157. @{account.get('acct')}
  158. {account.get('locked') ? <i className='fa fa-lock' /> : null}
  159. </span>
  160. <div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />
  161. {info}
  162. {actionBtn}
  163. </div>
  164. </div>
  165. {metadata.length && (
  166. <table className='account__metadata'>
  167. <tbody>
  168. {(() => {
  169. let data = [];
  170. for (let i = 0; i < metadata.length; i++) {
  171. data.push(
  172. <tr key={i}>
  173. <th scope='row'><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} /></th>
  174. <td><div dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} /></td>
  175. </tr>
  176. );
  177. }
  178. return data;
  179. })()}
  180. </tbody>
  181. </table>
  182. ) || null}
  183. </div>
  184. );
  185. }
  186. }