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.

262 lines
9.2 KiB

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