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.

450 lines
17 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 Report from './report';
  11. import FollowRequestContainer from '../containers/follow_request_container';
  12. import Icon from 'mastodon/components/icon';
  13. import { Link } from 'react-router-dom';
  14. import classNames from 'classnames';
  15. const messages = defineMessages({
  16. favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
  17. follow: { id: 'notification.follow', defaultMessage: '{name} followed you' },
  18. ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
  19. poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
  20. reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
  21. status: { id: 'notification.status', defaultMessage: '{name} just posted' },
  22. update: { id: 'notification.update', defaultMessage: '{name} edited a post' },
  23. adminSignUp: { id: 'notification.admin.sign_up', defaultMessage: '{name} signed up' },
  24. adminReport: { id: 'notification.admin.report', defaultMessage: '{name} reported {target}' },
  25. });
  26. const notificationForScreenReader = (intl, message, timestamp) => {
  27. const output = [message];
  28. output.push(intl.formatDate(timestamp, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }));
  29. return output.join(', ');
  30. };
  31. class Notification extends ImmutablePureComponent {
  32. static contextTypes = {
  33. router: PropTypes.object,
  34. };
  35. static propTypes = {
  36. notification: ImmutablePropTypes.map.isRequired,
  37. hidden: PropTypes.bool,
  38. onMoveUp: PropTypes.func.isRequired,
  39. onMoveDown: PropTypes.func.isRequired,
  40. onMention: PropTypes.func.isRequired,
  41. onFavourite: PropTypes.func.isRequired,
  42. onReblog: PropTypes.func.isRequired,
  43. onToggleHidden: PropTypes.func.isRequired,
  44. status: ImmutablePropTypes.map,
  45. intl: PropTypes.object.isRequired,
  46. getScrollPosition: PropTypes.func,
  47. updateScrollBottom: PropTypes.func,
  48. cacheMediaWidth: PropTypes.func,
  49. cachedMediaWidth: PropTypes.number,
  50. unread: PropTypes.bool,
  51. };
  52. handleMoveUp = () => {
  53. const { notification, onMoveUp } = this.props;
  54. onMoveUp(notification.get('id'));
  55. };
  56. handleMoveDown = () => {
  57. const { notification, onMoveDown } = this.props;
  58. onMoveDown(notification.get('id'));
  59. };
  60. handleOpen = () => {
  61. const { notification } = this.props;
  62. if (notification.get('status')) {
  63. this.context.router.history.push(`/@${notification.getIn(['status', 'account', 'acct'])}/${notification.get('status')}`);
  64. } else {
  65. this.handleOpenProfile();
  66. }
  67. };
  68. handleOpenProfile = () => {
  69. const { notification } = this.props;
  70. this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
  71. };
  72. handleMention = e => {
  73. e.preventDefault();
  74. const { notification, onMention } = this.props;
  75. onMention(notification.get('account'), this.context.router.history);
  76. };
  77. handleHotkeyFavourite = () => {
  78. const { status } = this.props;
  79. if (status) this.props.onFavourite(status);
  80. };
  81. handleHotkeyBoost = e => {
  82. const { status } = this.props;
  83. if (status) this.props.onReblog(status, e);
  84. };
  85. handleHotkeyToggleHidden = () => {
  86. const { status } = this.props;
  87. if (status) this.props.onToggleHidden(status);
  88. };
  89. getHandlers () {
  90. return {
  91. reply: this.handleMention,
  92. favourite: this.handleHotkeyFavourite,
  93. boost: this.handleHotkeyBoost,
  94. mention: this.handleMention,
  95. open: this.handleOpen,
  96. openProfile: this.handleOpenProfile,
  97. moveUp: this.handleMoveUp,
  98. moveDown: this.handleMoveDown,
  99. toggleHidden: this.handleHotkeyToggleHidden,
  100. };
  101. }
  102. renderFollow (notification, account, link) {
  103. const { intl, unread } = this.props;
  104. return (
  105. <HotKeys handlers={this.getHandlers()}>
  106. <div className={classNames('notification notification-follow focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
  107. <div className='notification__message'>
  108. <div className='notification__favourite-icon-wrapper'>
  109. <Icon id='user-plus' fixedWidth />
  110. </div>
  111. <span title={notification.get('created_at')}>
  112. <FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
  113. </span>
  114. </div>
  115. <AccountContainer id={account.get('id')} hidden={this.props.hidden} />
  116. </div>
  117. </HotKeys>
  118. );
  119. }
  120. renderFollowRequest (notification, account, link) {
  121. const { intl, unread } = this.props;
  122. return (
  123. <HotKeys handlers={this.getHandlers()}>
  124. <div className={classNames('notification notification-follow-request focusable', { unread })} 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'))}>
  125. <div className='notification__message'>
  126. <div className='notification__favourite-icon-wrapper'>
  127. <Icon id='user' fixedWidth />
  128. </div>
  129. <span title={notification.get('created_at')}>
  130. <FormattedMessage id='notification.follow_request' defaultMessage='{name} has requested to follow you' values={{ name: link }} />
  131. </span>
  132. </div>
  133. <FollowRequestContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
  134. </div>
  135. </HotKeys>
  136. );
  137. }
  138. renderMention (notification) {
  139. return (
  140. <StatusContainer
  141. id={notification.get('status')}
  142. withDismiss
  143. hidden={this.props.hidden}
  144. onMoveDown={this.handleMoveDown}
  145. onMoveUp={this.handleMoveUp}
  146. contextType='notifications'
  147. getScrollPosition={this.props.getScrollPosition}
  148. updateScrollBottom={this.props.updateScrollBottom}
  149. cachedMediaWidth={this.props.cachedMediaWidth}
  150. cacheMediaWidth={this.props.cacheMediaWidth}
  151. unread={this.props.unread}
  152. />
  153. );
  154. }
  155. renderFavourite (notification, link) {
  156. const { intl, unread } = this.props;
  157. return (
  158. <HotKeys handlers={this.getHandlers()}>
  159. <div className={classNames('notification notification-favourite focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
  160. <div className='notification__message'>
  161. <div className='notification__favourite-icon-wrapper'>
  162. <Icon id='star' className='star-icon' fixedWidth />
  163. </div>
  164. <span title={notification.get('created_at')}>
  165. <FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
  166. </span>
  167. </div>
  168. <StatusContainer
  169. id={notification.get('status')}
  170. account={notification.get('account')}
  171. muted
  172. withDismiss
  173. hidden={!!this.props.hidden}
  174. getScrollPosition={this.props.getScrollPosition}
  175. updateScrollBottom={this.props.updateScrollBottom}
  176. cachedMediaWidth={this.props.cachedMediaWidth}
  177. cacheMediaWidth={this.props.cacheMediaWidth}
  178. />
  179. </div>
  180. </HotKeys>
  181. );
  182. }
  183. renderReblog (notification, link) {
  184. const { intl, unread } = this.props;
  185. return (
  186. <HotKeys handlers={this.getHandlers()}>
  187. <div className={classNames('notification notification-reblog focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
  188. <div className='notification__message'>
  189. <div className='notification__favourite-icon-wrapper'>
  190. <Icon id='retweet' fixedWidth />
  191. </div>
  192. <span title={notification.get('created_at')}>
  193. <FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
  194. </span>
  195. </div>
  196. <StatusContainer
  197. id={notification.get('status')}
  198. account={notification.get('account')}
  199. muted
  200. withDismiss
  201. hidden={this.props.hidden}
  202. getScrollPosition={this.props.getScrollPosition}
  203. updateScrollBottom={this.props.updateScrollBottom}
  204. cachedMediaWidth={this.props.cachedMediaWidth}
  205. cacheMediaWidth={this.props.cacheMediaWidth}
  206. />
  207. </div>
  208. </HotKeys>
  209. );
  210. }
  211. renderStatus (notification, link) {
  212. const { intl, unread, status } = this.props;
  213. if (!status) {
  214. return null;
  215. }
  216. return (
  217. <HotKeys handlers={this.getHandlers()}>
  218. <div className={classNames('notification notification-status focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
  219. <div className='notification__message'>
  220. <div className='notification__favourite-icon-wrapper'>
  221. <Icon id='home' fixedWidth />
  222. </div>
  223. <span title={notification.get('created_at')}>
  224. <FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
  225. </span>
  226. </div>
  227. <StatusContainer
  228. id={notification.get('status')}
  229. account={notification.get('account')}
  230. contextType='notifications'
  231. muted
  232. withDismiss
  233. hidden={this.props.hidden}
  234. getScrollPosition={this.props.getScrollPosition}
  235. updateScrollBottom={this.props.updateScrollBottom}
  236. cachedMediaWidth={this.props.cachedMediaWidth}
  237. cacheMediaWidth={this.props.cacheMediaWidth}
  238. />
  239. </div>
  240. </HotKeys>
  241. );
  242. }
  243. renderUpdate (notification, link) {
  244. const { intl, unread, status } = this.props;
  245. if (!status) {
  246. return null;
  247. }
  248. return (
  249. <HotKeys handlers={this.getHandlers()}>
  250. <div className={classNames('notification notification-update focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.update, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
  251. <div className='notification__message'>
  252. <div className='notification__favourite-icon-wrapper'>
  253. <Icon id='pencil' fixedWidth />
  254. </div>
  255. <span title={notification.get('created_at')}>
  256. <FormattedMessage id='notification.update' defaultMessage='{name} edited a post' values={{ name: link }} />
  257. </span>
  258. </div>
  259. <StatusContainer
  260. id={notification.get('status')}
  261. account={notification.get('account')}
  262. contextType='notifications'
  263. muted
  264. withDismiss
  265. hidden={this.props.hidden}
  266. getScrollPosition={this.props.getScrollPosition}
  267. updateScrollBottom={this.props.updateScrollBottom}
  268. cachedMediaWidth={this.props.cachedMediaWidth}
  269. cacheMediaWidth={this.props.cacheMediaWidth}
  270. />
  271. </div>
  272. </HotKeys>
  273. );
  274. }
  275. renderPoll (notification, account) {
  276. const { intl, unread, status } = this.props;
  277. const ownPoll = me === account.get('id');
  278. const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
  279. if (!status) {
  280. return null;
  281. }
  282. return (
  283. <HotKeys handlers={this.getHandlers()}>
  284. <div className={classNames('notification notification-poll focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
  285. <div className='notification__message'>
  286. <div className='notification__favourite-icon-wrapper'>
  287. <Icon id='tasks' fixedWidth />
  288. </div>
  289. <span title={notification.get('created_at')}>
  290. {ownPoll ? (
  291. <FormattedMessage id='notification.own_poll' defaultMessage='Your poll has ended' />
  292. ) : (
  293. <FormattedMessage id='notification.poll' defaultMessage='A poll you have voted in has ended' />
  294. )}
  295. </span>
  296. </div>
  297. <StatusContainer
  298. id={notification.get('status')}
  299. account={account}
  300. contextType='notifications'
  301. muted
  302. withDismiss
  303. hidden={this.props.hidden}
  304. getScrollPosition={this.props.getScrollPosition}
  305. updateScrollBottom={this.props.updateScrollBottom}
  306. cachedMediaWidth={this.props.cachedMediaWidth}
  307. cacheMediaWidth={this.props.cacheMediaWidth}
  308. />
  309. </div>
  310. </HotKeys>
  311. );
  312. }
  313. renderAdminSignUp (notification, account, link) {
  314. const { intl, unread } = this.props;
  315. return (
  316. <HotKeys handlers={this.getHandlers()}>
  317. <div className={classNames('notification notification-admin-sign-up focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminSignUp, { name: account.get('acct') }), notification.get('created_at'))}>
  318. <div className='notification__message'>
  319. <div className='notification__favourite-icon-wrapper'>
  320. <Icon id='user-plus' fixedWidth />
  321. </div>
  322. <span title={notification.get('created_at')}>
  323. <FormattedMessage id='notification.admin.sign_up' defaultMessage='{name} signed up' values={{ name: link }} />
  324. </span>
  325. </div>
  326. <AccountContainer id={account.get('id')} hidden={this.props.hidden} />
  327. </div>
  328. </HotKeys>
  329. );
  330. }
  331. renderAdminReport (notification, account, link) {
  332. const { intl, unread, report } = this.props;
  333. if (!report) {
  334. return null;
  335. }
  336. const targetAccount = report.get('target_account');
  337. const targetDisplayNameHtml = { __html: targetAccount.get('display_name_html') };
  338. const targetLink = <bdi><Link className='notification__display-name' title={targetAccount.get('acct')} to={`/@${targetAccount.get('acct')}`} dangerouslySetInnerHTML={targetDisplayNameHtml} /></bdi>;
  339. return (
  340. <HotKeys handlers={this.getHandlers()}>
  341. <div className={classNames('notification notification-admin-report focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.adminReport, { name: account.get('acct'), target: notification.getIn(['report', 'target_account', 'acct']) }), notification.get('created_at'))}>
  342. <div className='notification__message'>
  343. <div className='notification__favourite-icon-wrapper'>
  344. <Icon id='flag' fixedWidth />
  345. </div>
  346. <span title={notification.get('created_at')}>
  347. <FormattedMessage id='notification.admin.report' defaultMessage='{name} reported {target}' values={{ name: link, target: targetLink }} />
  348. </span>
  349. </div>
  350. <Report account={account} report={notification.get('report')} hidden={this.props.hidden} />
  351. </div>
  352. </HotKeys>
  353. );
  354. }
  355. render () {
  356. const { notification } = this.props;
  357. const account = notification.get('account');
  358. const displayNameHtml = { __html: account.get('display_name_html') };
  359. const link = <bdi><Link className='notification__display-name' href={`/@${account.get('acct')}`} title={account.get('acct')} to={`/@${account.get('acct')}`} dangerouslySetInnerHTML={displayNameHtml} /></bdi>;
  360. switch(notification.get('type')) {
  361. case 'follow':
  362. return this.renderFollow(notification, account, link);
  363. case 'follow_request':
  364. return this.renderFollowRequest(notification, account, link);
  365. case 'mention':
  366. return this.renderMention(notification);
  367. case 'favourite':
  368. return this.renderFavourite(notification, link);
  369. case 'reblog':
  370. return this.renderReblog(notification, link);
  371. case 'status':
  372. return this.renderStatus(notification, link);
  373. case 'update':
  374. return this.renderUpdate(notification, link);
  375. case 'poll':
  376. return this.renderPoll(notification, account);
  377. case 'admin.sign_up':
  378. return this.renderAdminSignUp(notification, account, link);
  379. case 'admin.report':
  380. return this.renderAdminReport(notification, account, link);
  381. }
  382. return null;
  383. }
  384. }
  385. export default injectIntl(Notification);