1 import React
from 'react';
2 import ImmutablePropTypes
from 'react-immutable-proptypes';
3 import PropTypes
from 'prop-types';
4 import { defineMessages
, injectIntl
, FormattedMessage
} from 'react-intl';
5 import Button
from 'mastodon/components/button';
6 import ImmutablePureComponent
from 'react-immutable-pure-component';
7 import { autoPlayGif
, me
, isStaff
} from 'mastodon/initial_state';
8 import classNames
from 'classnames';
9 import Icon
from 'mastodon/components/icon';
10 import IconButton
from 'mastodon/components/icon_button';
11 import Avatar
from 'mastodon/components/avatar';
12 import { counterRenderer
} from 'mastodon/components/common_counter';
13 import ShortNumber
from 'mastodon/components/short_number';
14 import { NavLink
} from 'react-router-dom';
15 import DropdownMenuContainer
from 'mastodon/containers/dropdown_menu_container';
16 import AccountNoteContainer
from '../containers/account_note_container';
18 const messages
= defineMessages({
19 unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
20 follow: { id: 'account.follow', defaultMessage: 'Follow' },
21 cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
22 requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
23 unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
24 edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
25 linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
26 account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
27 mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
28 direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
29 unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
30 block: { id: 'account.block', defaultMessage: 'Block @{name}' },
31 mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
32 report: { id: 'account.report', defaultMessage: 'Report @{name}' },
33 share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
34 media: { id: 'account.media', defaultMessage: 'Media' },
35 blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
36 unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
37 hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
38 showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
39 enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
40 disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
41 pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
42 preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
43 follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
44 favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
45 lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
46 blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
47 domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
48 mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
49 endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
50 unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
51 add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
52 admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
55 const dateFormatOptions
= {
64 export default @injectIntl
65 class Header
extends ImmutablePureComponent
{
68 account: ImmutablePropTypes
.map
,
69 identity_props: ImmutablePropTypes
.list
,
70 onFollow: PropTypes
.func
.isRequired
,
71 onBlock: PropTypes
.func
.isRequired
,
72 onMention: PropTypes
.func
.isRequired
,
73 onDirect: PropTypes
.func
.isRequired
,
74 onReblogToggle: PropTypes
.func
.isRequired
,
75 onNotifyToggle: PropTypes
.func
.isRequired
,
76 onReport: PropTypes
.func
.isRequired
,
77 onMute: PropTypes
.func
.isRequired
,
78 onBlockDomain: PropTypes
.func
.isRequired
,
79 onUnblockDomain: PropTypes
.func
.isRequired
,
80 onEndorseToggle: PropTypes
.func
.isRequired
,
81 onAddToList: PropTypes
.func
.isRequired
,
82 onEditAccountNote: PropTypes
.func
.isRequired
,
83 intl: PropTypes
.object
.isRequired
,
84 domain: PropTypes
.string
.isRequired
,
87 openEditProfile
= () => {
88 window
.open('/settings/profile', '_blank');
91 isStatusesPageActive
= (match
, location
) => {
96 return !location
.pathname
.match(/\/(followers|following)\/?$/);
100 const node
= this.node
;
102 if (!node
|| autoPlayGif
) {
106 const emojis
= node
.querySelectorAll('.custom-emoji');
108 for (var i
= 0; i
< emojis
.length
; i
++) {
109 let emoji
= emojis
[i
];
110 if (emoji
.classList
.contains('status-emoji')) {
113 emoji
.classList
.add('status-emoji');
115 emoji
.addEventListener('mouseenter', this.handleEmojiMouseEnter
, false);
116 emoji
.addEventListener('mouseleave', this.handleEmojiMouseLeave
, false);
120 componentDidMount () {
121 this._updateEmojis();
124 componentDidUpdate () {
125 this._updateEmojis();
128 handleEmojiMouseEnter
= ({ target
}) => {
129 target
.src
= target
.getAttribute('data-original');
132 handleEmojiMouseLeave
= ({ target
}) => {
133 target
.src
= target
.getAttribute('data-static');
141 const { account
, intl
, domain
, identity_proofs
} = this.props
;
147 const suspended
= account
.get('suspended');
155 if (me
!== account
.get('id') && account
.getIn(['relationship', 'followed_by'])) {
156 info
.push(<span key
='followed_by' className
='relationship-tag'><FormattedMessage id
='account.follows_you' defaultMessage
='Follows you' /></span>);
157 } else if (me
!== account
.get('id') && account
.getIn(['relationship', 'blocking'])) {
158 info
.push(<span key
='blocked' className
='relationship-tag'><FormattedMessage id
='account.blocked' defaultMessage
='Blocked' /></span>);
161 if (me
!== account
.get('id') && account
.getIn(['relationship', 'muting'])) {
162 info
.push(<span key
='muted' className
='relationship-tag'><FormattedMessage id
='account.muted' defaultMessage
='Muted' /></span>);
163 } else if (me
!== account
.get('id') && account
.getIn(['relationship', 'domain_blocking'])) {
164 info
.push(<span key
='domain_blocked' className
='relationship-tag'><FormattedMessage id
='account.domain_blocked' defaultMessage
='Domain blocked' /></span>);
167 if (account
.getIn(['relationship', 'requested']) || account
.getIn(['relationship', 'following'])) {
168 bellBtn
= <IconButton icon
='bell-o' size
={24} active
={account
.getIn(['relationship', 'notifying'])} title
={intl
.formatMessage(account
.getIn(['relationship', 'notifying']) ? messages
.disableNotifications : messages
.enableNotifications
, { name: account
.get('username') })} onClick
={this.props
.onNotifyToggle
} />;
171 if (me
!== account
.get('id')) {
172 if (!account
.get('relationship')) { // Wait until the relationship is loaded
174 } else if (account
.getIn(['relationship', 'requested'])) {
175 actionBtn
= <Button className
={classNames('logo-button', { 'button--with-bell': bellBtn
!== '' })} text
={intl
.formatMessage(messages
.cancel_follow_request
)} title
={intl
.formatMessage(messages
.requested
)} onClick
={this.props
.onFollow
} />;
176 } else if (!account
.getIn(['relationship', 'blocking'])) {
177 actionBtn
= <Button disabled
={account
.getIn(['relationship', 'blocked_by'])} className
={classNames('logo-button', { 'button--destructive': account
.getIn(['relationship', 'following']), 'button--with-bell': bellBtn
!== '' })} text
={intl
.formatMessage(account
.getIn(['relationship', 'following']) ? messages
.unfollow : messages
.follow
)} onClick
={this.props
.onFollow
} />;
178 } else if (account
.getIn(['relationship', 'blocking'])) {
179 actionBtn
= <Button className
='logo-button' text
={intl
.formatMessage(messages
.unblock
, { name: account
.get('username') })} onClick
={this.props
.onBlock
} />;
182 actionBtn
= <Button className
='logo-button' text
={intl
.formatMessage(messages
.edit_profile
)} onClick
={this.openEditProfile
} />;
185 if (account
.get('moved') && !account
.getIn(['relationship', 'following'])) {
189 if (account
.get('locked')) {
190 lockedIcon
= <Icon id
='lock' title
={intl
.formatMessage(messages
.account_locked
)} />;
193 if (account
.get('id') !== me
) {
194 menu
.push({ text: intl
.formatMessage(messages
.mention
, { name: account
.get('username') }), action: this.props
.onMention
});
195 menu
.push({ text: intl
.formatMessage(messages
.direct
, { name: account
.get('username') }), action: this.props
.onDirect
});
199 if ('share' in navigator
) {
200 menu
.push({ text: intl
.formatMessage(messages
.share
, { name: account
.get('username') }), action: this.handleShare
});
204 if (account
.get('id') === me
) {
205 menu
.push({ text: intl
.formatMessage(messages
.edit_profile
), href: '/settings/profile' });
206 menu
.push({ text: intl
.formatMessage(messages
.preferences
), href: '/settings/preferences' });
207 menu
.push({ text: intl
.formatMessage(messages
.pins
), to: '/pinned' });
209 menu
.push({ text: intl
.formatMessage(messages
.follow_requests
), to: '/follow_requests' });
210 menu
.push({ text: intl
.formatMessage(messages
.favourites
), to: '/favourites' });
211 menu
.push({ text: intl
.formatMessage(messages
.lists
), to: '/lists' });
213 menu
.push({ text: intl
.formatMessage(messages
.mutes
), to: '/mutes' });
214 menu
.push({ text: intl
.formatMessage(messages
.blocks
), to: '/blocks' });
215 menu
.push({ text: intl
.formatMessage(messages
.domain_blocks
), to: '/domain_blocks' });
217 if (account
.getIn(['relationship', 'following'])) {
218 if (!account
.getIn(['relationship', 'muting'])) {
219 if (account
.getIn(['relationship', 'showing_reblogs'])) {
220 menu
.push({ text: intl
.formatMessage(messages
.hideReblogs
, { name: account
.get('username') }), action: this.props
.onReblogToggle
});
222 menu
.push({ text: intl
.formatMessage(messages
.showReblogs
, { name: account
.get('username') }), action: this.props
.onReblogToggle
});
226 menu
.push({ text: intl
.formatMessage(account
.getIn(['relationship', 'endorsed']) ? messages
.unendorse : messages
.endorse
), action: this.props
.onEndorseToggle
});
227 menu
.push({ text: intl
.formatMessage(messages
.add_or_remove_from_list
), action: this.props
.onAddToList
});
231 if (account
.getIn(['relationship', 'muting'])) {
232 menu
.push({ text: intl
.formatMessage(messages
.unmute
, { name: account
.get('username') }), action: this.props
.onMute
});
234 menu
.push({ text: intl
.formatMessage(messages
.mute
, { name: account
.get('username') }), action: this.props
.onMute
});
237 if (account
.getIn(['relationship', 'blocking'])) {
238 menu
.push({ text: intl
.formatMessage(messages
.unblock
, { name: account
.get('username') }), action: this.props
.onBlock
});
240 menu
.push({ text: intl
.formatMessage(messages
.block
, { name: account
.get('username') }), action: this.props
.onBlock
});
243 menu
.push({ text: intl
.formatMessage(messages
.report
, { name: account
.get('username') }), action: this.props
.onReport
});
246 if (account
.get('acct') !== account
.get('username')) {
247 const domain
= account
.get('acct').split('@')[1];
251 if (account
.getIn(['relationship', 'domain_blocking'])) {
252 menu
.push({ text: intl
.formatMessage(messages
.unblockDomain
, { domain
}), action: this.props
.onUnblockDomain
});
254 menu
.push({ text: intl
.formatMessage(messages
.blockDomain
, { domain
}), action: this.props
.onBlockDomain
});
258 if (account
.get('id') !== me
&& isStaff
) {
260 menu
.push({ text: intl
.formatMessage(messages
.admin_account
, { name: account
.get('username') }), href: `/admin/accounts/${account.get('id')}` });
263 const content
= { __html: account
.get('note_emojified') };
264 const displayNameHtml
= { __html: account
.get('display_name_html') };
265 const fields
= account
.get('fields');
266 const acct
= account
.get('acct').indexOf('@') === -1 && domain
? `${account.get('acct')}@${domain}` : account
.get('acct');
270 if (account
.get('bot')) {
271 badge
= (<div className
='account-role bot'><FormattedMessage id
='account.badges.bot' defaultMessage
='Bot' /></div>);
272 } else if (account
.get('group')) {
273 badge
= (<div className
='account-role group'><FormattedMessage id
='account.badges.group' defaultMessage
='Group' /></div>);
279 <div className
={classNames('account__header', { inactive: !!account
.get('moved') })} ref
={this.setRef
}>
280 <div className
='account__header__image'>
281 <div className
='account__header__info'>
285 <img src
={autoPlayGif
? account
.get('header') : account
.get('header_static')} alt
='' className
='parallax' />
288 <div className
='account__header__bar'>
289 <div className
='account__header__tabs'>
290 <a className
='avatar' href
={account
.get('url')} rel
='noopener noreferrer' target
='_blank'>
291 <Avatar account
={account
} size
={90} />
294 <div className
='spacer' />
297 <div className
='account__header__tabs__buttons'>
301 <DropdownMenuContainer items
={menu
} icon
='ellipsis-v' size
={24} direction
='right' />
306 <div className
='account__header__tabs__name'>
308 <span dangerouslySetInnerHTML
={displayNameHtml
} /> {badge
}
309 <small
>@{acct
} {lockedIcon
}</small
>
313 <div className
='account__header__extra'>
314 <div className
='account__header__bio'>
315 {(fields
.size
> 0 || identity_proofs
.size
> 0) && (
316 <div className
='account__header__fields'>
317 {identity_proofs
.map((proof
, i
) => (
319 <dt dangerouslySetInnerHTML
={{ __html: proof
.get('provider') }} />
321 <dd className
='verified'>
322 <a href
={proof
.get('proof_url')} target
='_blank' rel
='noopener noreferrer'><span title
={intl
.formatMessage(messages
.linkVerifiedOn
, { date: intl
.formatDate(proof
.get('updated_at'), dateFormatOptions
) })}>
323 <Icon id
='check' className
='verified__mark' />
325 <a href
={proof
.get('profile_url')} target
='_blank' rel
='noopener noreferrer'><span dangerouslySetInnerHTML
={{ __html: ' '+proof
.get('provider_username') }} /></a>
329 {fields
.map((pair
, i
) => (
331 <dt dangerouslySetInnerHTML
={{ __html: pair
.get('name_emojified') }} title
={pair
.get('name')} className
='translate' />
333 <dd className
={pair
.get('verified_at') && 'verified'} title
={pair
.get('value_plain')} className
='translate'>
334 {pair
.get('verified_at') && <span title
={intl
.formatMessage(messages
.linkVerifiedOn
, { date: intl
.formatDate(pair
.get('verified_at'), dateFormatOptions
) })}><Icon id
='check' className
='verified__mark' /></span>} <span dangerouslySetInnerHTML
={{ __html: pair
.get('value_emojified') }} />
341 {account
.get('id') !== me
&& !suspended
&& <AccountNoteContainer account
={account
} />}
343 {account
.get('note').length
> 0 && account
.get('note') !== '<p></p>' && <div className
='account__header__content translate' dangerouslySetInnerHTML
={content
} />}
347 <div className
='account__header__extra__links'>
348 <NavLink isActive
={this.isStatusesPageActive
} activeClassName
='active' to
={`/accounts/${account.get('id')}`} title
={intl
.formatNumber(account
.get('statuses_count'))}>
350 value
={account
.get('statuses_count')}
351 renderer
={counterRenderer('statuses')}
355 <NavLink exact activeClassName
='active' to
={`/accounts/${account.get('id')}/following`} title
={intl
.formatNumber(account
.get('following_count'))}>
357 value
={account
.get('following_count')}
358 renderer
={counterRenderer('following')}
362 <NavLink exact activeClassName
='active' to
={`/accounts/${account.get('id')}/followers`} title
={intl
.formatNumber(account
.get('followers_count'))}>
364 value
={account
.get('followers_count')}
365 renderer
={counterRenderer('followers')}