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.

302 lines
11 KiB

  1. import React from 'react';
  2. import ImmutablePropTypes from 'react-immutable-proptypes';
  3. import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
  4. import { HotKeys } from 'react-hotkeys';
  5. import PropTypes from 'prop-types';
  6. import ImmutablePureComponent from 'react-immutable-pure-component';
  7. import { me } from 'mastodon/initial_state';
  8. import StatusContainer from 'mastodon/containers/status_container';
  9. import AccountContainer from 'mastodon/containers/account_container';
  10. import FollowRequestContainer from '../containers/follow_request_container';
  11. import Icon from 'mastodon/components/icon';
  12. import Permalink from 'mastodon/components/permalink';
  13. const messages = defineMessages({
  14. favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
  15. follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
  16. ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
  17. poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
  18. reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
  19. });
  20. const notificationForScreenReader = (intl, message, timestamp) => {
  21. const output = [message];
  22. output.push(intl.formatDate(timestamp, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }));
  23. return output.join(', ');
  24. };
  25. export default @injectIntl
  26. class Notification extends ImmutablePureComponent {
  27. static contextTypes = {
  28. router: PropTypes.object,
  29. };
  30. static propTypes = {
  31. notification: ImmutablePropTypes.map.isRequired,
  32. hidden: PropTypes.bool,
  33. onMoveUp: PropTypes.func.isRequired,
  34. onMoveDown: PropTypes.func.isRequired,
  35. onMention: PropTypes.func.isRequired,
  36. onFavourite: PropTypes.func.isRequired,
  37. onReblog: PropTypes.func.isRequired,
  38. onToggleHidden: PropTypes.func.isRequired,
  39. status: ImmutablePropTypes.map,
  40. intl: PropTypes.object.isRequired,
  41. getScrollPosition: PropTypes.func,
  42. updateScrollBottom: PropTypes.func,
  43. cacheMediaWidth: PropTypes.func,
  44. cachedMediaWidth: PropTypes.number,
  45. };
  46. handleMoveUp = () => {
  47. const { notification, onMoveUp } = this.props;
  48. onMoveUp(notification.get('id'));
  49. }
  50. handleMoveDown = () => {
  51. const { notification, onMoveDown } = this.props;
  52. onMoveDown(notification.get('id'));
  53. }
  54. handleOpen = () => {
  55. const { notification } = this.props;
  56. if (notification.get('status')) {
  57. this.context.router.history.push(`/statuses/${notification.get('status')}`);
  58. } else {
  59. this.handleOpenProfile();
  60. }
  61. }
  62. handleOpenProfile = () => {
  63. const { notification } = this.props;
  64. this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`);
  65. }
  66. handleMention = e => {
  67. e.preventDefault();
  68. const { notification, onMention } = this.props;
  69. onMention(notification.get('account'), this.context.router.history);
  70. }
  71. handleHotkeyFavourite = () => {
  72. const { status } = this.props;
  73. if (status) this.props.onFavourite(status);
  74. }
  75. handleHotkeyBoost = e => {
  76. const { status } = this.props;
  77. if (status) this.props.onReblog(status, e);
  78. }
  79. handleHotkeyToggleHidden = () => {
  80. const { status } = this.props;
  81. if (status) this.props.onToggleHidden(status);
  82. }
  83. getHandlers () {
  84. return {
  85. reply: this.handleMention,
  86. favourite: this.handleHotkeyFavourite,
  87. boost: this.handleHotkeyBoost,
  88. mention: this.handleMention,
  89. open: this.handleOpen,
  90. openProfile: this.handleOpenProfile,
  91. moveUp: this.handleMoveUp,
  92. moveDown: this.handleMoveDown,
  93. toggleHidden: this.handleHotkeyToggleHidden,
  94. };
  95. }
  96. renderFollow (notification, account, link) {
  97. const { intl } = this.props;
  98. return (
  99. <HotKeys handlers={this.getHandlers()}>
  100. <div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
  101. <div className='notification__message'>
  102. <div className='notification__favourite-icon-wrapper'>
  103. <Icon id='user-plus' fixedWidth />
  104. </div>
  105. <span title={notification.get('created_at')}>
  106. <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
  107. </span>
  108. </div>
  109. <AccountContainer id={account.get('id')} hidden={this.props.hidden} />
  110. </div>
  111. </HotKeys>
  112. );
  113. }
  114. renderFollowRequest (notification, account, link) {
  115. const { intl } = this.props;
  116. return (
  117. <HotKeys handlers={this.getHandlers()}>
  118. <div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
  119. <div className='notification__message'>
  120. <div className='notification__favourite-icon-wrapper'>
  121. <Icon id='user' fixedWidth />
  122. </div>
  123. <span title={notification.get('created_at')}>
  124. <FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} />
  125. </span>
  126. </div>
  127. <FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
  128. </div>
  129. </HotKeys>
  130. );
  131. }
  132. renderMention (notification) {
  133. return (
  134. <StatusContainer
  135. id={notification.get('status')}
  136. withDismiss
  137. hidden={this.props.hidden}
  138. onMoveDown={this.handleMoveDown}
  139. onMoveUp={this.handleMoveUp}
  140. contextType='notifications'
  141. getScrollPosition={this.props.getScrollPosition}
  142. updateScrollBottom={this.props.updateScrollBottom}
  143. cachedMediaWidth={this.props.cachedMediaWidth}
  144. cacheMediaWidth={this.props.cacheMediaWidth}
  145. />
  146. );
  147. }
  148. renderFavourite (notification, link) {
  149. const { intl } = this.props;
  150. return (
  151. <HotKeys handlers={this.getHandlers()}>
  152. <div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
  153. <div className='notification__message'>
  154. <div className='notification__favourite-icon-wrapper'>
  155. <Icon id='star' className='star-icon' fixedWidth />
  156. </div>
  157. <span title={notification.get('created_at')}>
  158. <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
  159. </span>
  160. </div>
  161. <StatusContainer
  162. id={notification.get('status')}
  163. account={notification.get('account')}
  164. muted
  165. withDismiss
  166. hidden={!!this.props.hidden}
  167. getScrollPosition={this.props.getScrollPosition}
  168. updateScrollBottom={this.props.updateScrollBottom}
  169. cachedMediaWidth={this.props.cachedMediaWidth}
  170. cacheMediaWidth={this.props.cacheMediaWidth}
  171. />
  172. </div>
  173. </HotKeys>
  174. );
  175. }
  176. renderReblog (notification, link) {
  177. const { intl } = this.props;
  178. return (
  179. <HotKeys handlers={this.getHandlers()}>
  180. <div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
  181. <div className='notification__message'>
  182. <div className='notification__favourite-icon-wrapper'>
  183. <Icon id='retweet' fixedWidth />
  184. </div>
  185. <span title={notification.get('created_at')}>
  186. <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
  187. </span>
  188. </div>
  189. <StatusContainer
  190. id={notification.get('status')}
  191. account={notification.get('account')}
  192. muted
  193. withDismiss
  194. hidden={this.props.hidden}
  195. getScrollPosition={this.props.getScrollPosition}
  196. updateScrollBottom={this.props.updateScrollBottom}
  197. cachedMediaWidth={this.props.cachedMediaWidth}
  198. cacheMediaWidth={this.props.cacheMediaWidth}
  199. />
  200. </div>
  201. </HotKeys>
  202. );
  203. }
  204. renderPoll (notification, account) {
  205. const { intl } = this.props;
  206. const ownPoll = me === account.get('id');
  207. const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
  208. return (
  209. <HotKeys handlers={this.getHandlers()}>
  210. <div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
  211. <div className='notification__message'>
  212. <div className='notification__favourite-icon-wrapper'>
  213. <Icon id='tasks' fixedWidth />
  214. </div>
  215. <span title={notification.get('created_at')}>
  216. {ownPoll ? (
  217. <FormattedMessage id='notification.own_poll' defaultMessage='Your poll has ended' />
  218. ) : (
  219. <FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' />
  220. )}
  221. </span>
  222. </div>
  223. <StatusContainer
  224. id={notification.get('status')}
  225. account={account}
  226. muted
  227. withDismiss
  228. hidden={this.props.hidden}
  229. getScrollPosition={this.props.getScrollPosition}
  230. updateScrollBottom={this.props.updateScrollBottom}
  231. cachedMediaWidth={this.props.cachedMediaWidth}
  232. cacheMediaWidth={this.props.cacheMediaWidth}
  233. />
  234. </div>
  235. </HotKeys>
  236. );
  237. }
  238. render () {
  239. const { notification } = this.props;
  240. const account = notification.get('account');
  241. const displayNameHtml = { __html: account.get('display_name_html') };
  242. const link = <bdi><Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
  243. switch(notification.get('type')) {
  244. case 'follow':
  245. return this.renderFollow(notification, account, link);
  246. case 'follow_request':
  247. return this.renderFollowRequest(notification, account, link);
  248. case 'mention':
  249. return this.renderMention(notification);
  250. case 'favourite':
  251. return this.renderFavourite(notification, link);
  252. case 'reblog':
  253. return this.renderReblog(notification, link);
  254. case 'poll':
  255. return this.renderPoll(notification, account);
  256. }
  257. return null;
  258. }
  259. }