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 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' },
24 const notificationForScreenReader
= (intl
, message
, timestamp
) => {
25 const output
= [message
];
27 output
.push(intl
.formatDate(timestamp
, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }));
29 return output
.join(', ');
32 export default @injectIntl
33 class Notification
extends ImmutablePureComponent
{
35 static contextTypes
= {
36 router: PropTypes
.object
,
40 notification: ImmutablePropTypes
.map
.isRequired
,
41 hidden: PropTypes
.bool
,
42 onMoveUp: PropTypes
.func
.isRequired
,
43 onMoveDown: PropTypes
.func
.isRequired
,
44 onMention: PropTypes
.func
.isRequired
,
45 onFavourite: PropTypes
.func
.isRequired
,
46 onReblog: PropTypes
.func
.isRequired
,
47 onToggleHidden: PropTypes
.func
.isRequired
,
48 status: ImmutablePropTypes
.map
,
49 intl: PropTypes
.object
.isRequired
,
50 getScrollPosition: PropTypes
.func
,
51 updateScrollBottom: PropTypes
.func
,
52 cacheMediaWidth: PropTypes
.func
,
53 cachedMediaWidth: PropTypes
.number
,
54 unread: PropTypes
.bool
,
57 handleMoveUp
= () => {
58 const { notification
, onMoveUp
} = this.props
;
59 onMoveUp(notification
.get('id'));
62 handleMoveDown
= () => {
63 const { notification
, onMoveDown
} = this.props
;
64 onMoveDown(notification
.get('id'));
68 const { notification
} = this.props
;
70 if (notification
.get('status')) {
71 this.context
.router
.history
.push(`/@${notification.getIn(['status', 'account', 'acct'])}/${notification.get('status')}`);
73 this.handleOpenProfile();
77 handleOpenProfile
= () => {
78 const { notification
} = this.props
;
79 this.context
.router
.history
.push(`/@${notification.getIn(['account', 'acct'])}`);
82 handleMention
= e
=> {
85 const { notification
, onMention
} = this.props
;
86 onMention(notification
.get('account'), this.context
.router
.history
);
89 handleHotkeyFavourite
= () => {
90 const { status
} = this.props
;
91 if (status
) this.props
.onFavourite(status
);
94 handleHotkeyBoost
= e
=> {
95 const { status
} = this.props
;
96 if (status
) this.props
.onReblog(status
, e
);
99 handleHotkeyToggleHidden
= () => {
100 const { status
} = this.props
;
101 if (status
) this.props
.onToggleHidden(status
);
106 reply: this.handleMention
,
107 favourite: this.handleHotkeyFavourite
,
108 boost: this.handleHotkeyBoost
,
109 mention: this.handleMention
,
110 open: this.handleOpen
,
111 openProfile: this.handleOpenProfile
,
112 moveUp: this.handleMoveUp
,
113 moveDown: this.handleMoveDown
,
114 toggleHidden: this.handleHotkeyToggleHidden
,
118 renderFollow (notification
, account
, link
) {
119 const { intl
, unread
} = this.props
;
122 <HotKeys handlers
={this.getHandlers()}>
123 <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'))}>
124 <div className
='notification__message'>
125 <div className
='notification__favourite-icon-wrapper'>
126 <Icon id
='user-plus' fixedWidth
/>
129 <span title
={notification
.get('created_at')}>
130 <FormattedMessage id
='notification.follow' defaultMessage
='{name} followed you' values
={{ name: link
}} />
134 <AccountContainer id
={account
.get('id')} hidden
={this.props
.hidden
} />
140 renderFollowRequest (notification
, account
, link
) {
141 const { intl
, unread
} = this.props
;
144 <HotKeys handlers
={this.getHandlers()}>
145 <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'))}>
146 <div className
='notification__message'>
147 <div className
='notification__favourite-icon-wrapper'>
148 <Icon id
='user' fixedWidth
/>
151 <span title
={notification
.get('created_at')}>
152 <FormattedMessage id
='notification.follow_request' defaultMessage
='{name} has requested to follow you' values
={{ name: link
}} />
156 <FollowRequestContainer id
={account
.get('id')} withNote
={false} hidden
={this.props
.hidden
} />
162 renderMention (notification
) {
165 id
={notification
.get('status')}
167 hidden
={this.props
.hidden
}
168 onMoveDown
={this.handleMoveDown
}
169 onMoveUp
={this.handleMoveUp
}
170 contextType
='notifications'
171 getScrollPosition
={this.props
.getScrollPosition
}
172 updateScrollBottom
={this.props
.updateScrollBottom
}
173 cachedMediaWidth
={this.props
.cachedMediaWidth
}
174 cacheMediaWidth
={this.props
.cacheMediaWidth
}
175 unread
={this.props
.unread
}
180 renderFavourite (notification
, link
) {
181 const { intl
, unread
} = this.props
;
184 <HotKeys handlers
={this.getHandlers()}>
185 <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'))}>
186 <div className
='notification__message'>
187 <div className
='notification__favourite-icon-wrapper'>
188 <Icon id
='star' className
='star-icon' fixedWidth
/>
191 <span title
={notification
.get('created_at')}>
192 <FormattedMessage id
='notification.favourite' defaultMessage
='{name} favourited your status' values
={{ name: link
}} />
197 id
={notification
.get('status')}
198 account
={notification
.get('account')}
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
}
212 renderReblog (notification
, link
) {
213 const { intl
, unread
} = this.props
;
216 <HotKeys handlers
={this.getHandlers()}>
217 <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'))}>
218 <div className
='notification__message'>
219 <div className
='notification__favourite-icon-wrapper'>
220 <Icon id
='retweet' fixedWidth
/>
223 <span title
={notification
.get('created_at')}>
224 <FormattedMessage id
='notification.reblog' defaultMessage
='{name} boosted your status' values
={{ name: link
}} />
229 id
={notification
.get('status')}
230 account
={notification
.get('account')}
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
}
244 renderStatus (notification
, link
) {
245 const { intl
, unread
} = this.props
;
248 <HotKeys handlers
={this.getHandlers()}>
249 <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'))}>
250 <div className
='notification__message'>
251 <div className
='notification__favourite-icon-wrapper'>
252 <Icon id
='home' fixedWidth
/>
255 <span title
={notification
.get('created_at')}>
256 <FormattedMessage id
='notification.status' defaultMessage
='{name} just posted' values
={{ name: link
}} />
261 id
={notification
.get('status')}
262 account
={notification
.get('account')}
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
}
276 renderPoll (notification
, account
) {
277 const { intl
, unread
} = this.props
;
278 const ownPoll
= me
=== account
.get('id');
279 const message
= ownPoll
? intl
.formatMessage(messages
.ownPoll
) : intl
.formatMessage(messages
.poll
);
282 <HotKeys handlers
={this.getHandlers()}>
283 <div className
={classNames('notification notification-poll focusable', { unread
})} tabIndex
='0' aria
-label
={notificationForScreenReader(intl
, message
, notification
.get('created_at'))}>
284 <div className
='notification__message'>
285 <div className
='notification__favourite-icon-wrapper'>
286 <Icon id
='tasks' fixedWidth
/>
289 <span title
={notification
.get('created_at')}>
291 <FormattedMessage id
='notification.own_poll' defaultMessage
='Your poll has ended' />
293 <FormattedMessage id
='notification.poll' defaultMessage
='A poll you have voted in has ended' />
299 id
={notification
.get('status')}
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
}
315 const { notification
} = this.props
;
316 const account
= notification
.get('account');
317 const displayNameHtml
= { __html: account
.get('display_name_html') };
318 const link
= <bdi
><Permalink className
='notification__display-name' href
={account
.get('url')} title
={account
.get('acct')} to
={`/@${account.get('acct')}`} dangerouslySetInnerHTML
={displayNameHtml
} /></bdi
>;
320 switch(notification
.get('type')) {
322 return this.renderFollow(notification
, account
, link
);
323 case 'follow_request':
324 return this.renderFollowRequest(notification
, account
, link
);
326 return this.renderMention(notification
);
328 return this.renderFavourite(notification
, link
);
330 return this.renderReblog(notification
, link
);
332 return this.renderStatus(notification
, link
);
334 return this.renderPoll(notification
, account
);