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)\/?$/);
99 handleMouseEnter
= ({ currentTarget
}) => {
104 const emojis
= currentTarget
.querySelectorAll('.custom-emoji');
106 for (var i
= 0; i
< emojis
.length
; i
++) {
107 let emoji
= emojis
[i
];
108 emoji
.src
= emoji
.getAttribute('data-original');
112 handleMouseLeave
= ({ currentTarget
}) => {
117 const emojis
= currentTarget
.querySelectorAll('.custom-emoji');
119 for (var i
= 0; i
< emojis
.length
; i
++) {
120 let emoji
= emojis
[i
];
121 emoji
.src
= emoji
.getAttribute('data-static');
126 const { account
, intl
, domain
, identity_proofs
} = this.props
;
132 const suspended
= account
.get('suspended');
140 if (me
!== account
.get('id') && account
.getIn(['relationship', 'followed_by'])) {
141 info
.push(<span key
='followed_by' className
='relationship-tag'><FormattedMessage id
='account.follows_you' defaultMessage
='Follows you' /></span>);
142 } else if (me
!== account
.get('id') && account
.getIn(['relationship', 'blocking'])) {
143 info
.push(<span key
='blocked' className
='relationship-tag'><FormattedMessage id
='account.blocked' defaultMessage
='Blocked' /></span>);
146 if (me
!== account
.get('id') && account
.getIn(['relationship', 'muting'])) {
147 info
.push(<span key
='muted' className
='relationship-tag'><FormattedMessage id
='account.muted' defaultMessage
='Muted' /></span>);
148 } else if (me
!== account
.get('id') && account
.getIn(['relationship', 'domain_blocking'])) {
149 info
.push(<span key
='domain_blocked' className
='relationship-tag'><FormattedMessage id
='account.domain_blocked' defaultMessage
='Domain blocked' /></span>);
152 if (account
.getIn(['relationship', 'requested']) || account
.getIn(['relationship', 'following'])) {
153 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
} />;
156 if (me
!== account
.get('id')) {
157 if (!account
.get('relationship')) { // Wait until the relationship is loaded
159 } else if (account
.getIn(['relationship', 'requested'])) {
160 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
} />;
161 } else if (!account
.getIn(['relationship', 'blocking'])) {
162 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
} />;
163 } else if (account
.getIn(['relationship', 'blocking'])) {
164 actionBtn
= <Button className
='logo-button' text
={intl
.formatMessage(messages
.unblock
, { name: account
.get('username') })} onClick
={this.props
.onBlock
} />;
167 actionBtn
= <Button className
='logo-button' text
={intl
.formatMessage(messages
.edit_profile
)} onClick
={this.openEditProfile
} />;
170 if (account
.get('moved') && !account
.getIn(['relationship', 'following'])) {
174 if (account
.get('locked')) {
175 lockedIcon
= <Icon id
='lock' title
={intl
.formatMessage(messages
.account_locked
)} />;
178 if (account
.get('id') !== me
) {
179 menu
.push({ text: intl
.formatMessage(messages
.mention
, { name: account
.get('username') }), action: this.props
.onMention
});
180 menu
.push({ text: intl
.formatMessage(messages
.direct
, { name: account
.get('username') }), action: this.props
.onDirect
});
184 if ('share' in navigator
) {
185 menu
.push({ text: intl
.formatMessage(messages
.share
, { name: account
.get('username') }), action: this.handleShare
});
189 if (account
.get('id') === me
) {
190 menu
.push({ text: intl
.formatMessage(messages
.edit_profile
), href: '/settings/profile' });
191 menu
.push({ text: intl
.formatMessage(messages
.preferences
), href: '/settings/preferences' });
192 menu
.push({ text: intl
.formatMessage(messages
.pins
), to: '/pinned' });
194 menu
.push({ text: intl
.formatMessage(messages
.follow_requests
), to: '/follow_requests' });
195 menu
.push({ text: intl
.formatMessage(messages
.favourites
), to: '/favourites' });
196 menu
.push({ text: intl
.formatMessage(messages
.lists
), to: '/lists' });
198 menu
.push({ text: intl
.formatMessage(messages
.mutes
), to: '/mutes' });
199 menu
.push({ text: intl
.formatMessage(messages
.blocks
), to: '/blocks' });
200 menu
.push({ text: intl
.formatMessage(messages
.domain_blocks
), to: '/domain_blocks' });
202 if (account
.getIn(['relationship', 'following'])) {
203 if (!account
.getIn(['relationship', 'muting'])) {
204 if (account
.getIn(['relationship', 'showing_reblogs'])) {
205 menu
.push({ text: intl
.formatMessage(messages
.hideReblogs
, { name: account
.get('username') }), action: this.props
.onReblogToggle
});
207 menu
.push({ text: intl
.formatMessage(messages
.showReblogs
, { name: account
.get('username') }), action: this.props
.onReblogToggle
});
211 menu
.push({ text: intl
.formatMessage(account
.getIn(['relationship', 'endorsed']) ? messages
.unendorse : messages
.endorse
), action: this.props
.onEndorseToggle
});
212 menu
.push({ text: intl
.formatMessage(messages
.add_or_remove_from_list
), action: this.props
.onAddToList
});
216 if (account
.getIn(['relationship', 'muting'])) {
217 menu
.push({ text: intl
.formatMessage(messages
.unmute
, { name: account
.get('username') }), action: this.props
.onMute
});
219 menu
.push({ text: intl
.formatMessage(messages
.mute
, { name: account
.get('username') }), action: this.props
.onMute
});
222 if (account
.getIn(['relationship', 'blocking'])) {
223 menu
.push({ text: intl
.formatMessage(messages
.unblock
, { name: account
.get('username') }), action: this.props
.onBlock
});
225 menu
.push({ text: intl
.formatMessage(messages
.block
, { name: account
.get('username') }), action: this.props
.onBlock
});
228 menu
.push({ text: intl
.formatMessage(messages
.report
, { name: account
.get('username') }), action: this.props
.onReport
});
231 if (account
.get('acct') !== account
.get('username')) {
232 const domain
= account
.get('acct').split('@')[1];
236 if (account
.getIn(['relationship', 'domain_blocking'])) {
237 menu
.push({ text: intl
.formatMessage(messages
.unblockDomain
, { domain
}), action: this.props
.onUnblockDomain
});
239 menu
.push({ text: intl
.formatMessage(messages
.blockDomain
, { domain
}), action: this.props
.onBlockDomain
});
243 if (account
.get('id') !== me
&& isStaff
) {
245 menu
.push({ text: intl
.formatMessage(messages
.admin_account
, { name: account
.get('username') }), href: `/admin/accounts/${account.get('id')}` });
248 const content
= { __html: account
.get('note_emojified') };
249 const displayNameHtml
= { __html: account
.get('display_name_html') };
250 const fields
= account
.get('fields');
251 const acct
= account
.get('acct').indexOf('@') === -1 && domain
? `${account.get('acct')}@${domain}` : account
.get('acct');
255 if (account
.get('bot')) {
256 badge
= (<div className
='account-role bot'><FormattedMessage id
='account.badges.bot' defaultMessage
='Bot' /></div>);
257 } else if (account
.get('group')) {
258 badge
= (<div className
='account-role group'><FormattedMessage id
='account.badges.group' defaultMessage
='Group' /></div>);
264 <div className
={classNames('account__header', { inactive: !!account
.get('moved') })} onMouseEnter
={this.handleMouseEnter
} onMouseLeave
={this.handleMouseLeave
}>
265 <div className
='account__header__image'>
266 <div className
='account__header__info'>
270 <img src
={autoPlayGif
? account
.get('header') : account
.get('header_static')} alt
='' className
='parallax' />
273 <div className
='account__header__bar'>
274 <div className
='account__header__tabs'>
275 <a className
='avatar' href
={account
.get('url')} rel
='noopener noreferrer' target
='_blank'>
276 <Avatar account
={account
} size
={90} />
279 <div className
='spacer' />
282 <div className
='account__header__tabs__buttons'>
286 <DropdownMenuContainer items
={menu
} icon
='ellipsis-v' size
={24} direction
='right' />
291 <div className
='account__header__tabs__name'>
293 <span dangerouslySetInnerHTML
={displayNameHtml
} /> {badge
}
294 <small
>@{acct
} {lockedIcon
}</small
>
298 <div className
='account__header__extra'>
299 <div className
='account__header__bio'>
300 {(fields
.size
> 0 || identity_proofs
.size
> 0) && (
301 <div className
='account__header__fields'>
302 {identity_proofs
.map((proof
, i
) => (
304 <dt dangerouslySetInnerHTML
={{ __html: proof
.get('provider') }} />
306 <dd className
='verified'>
307 <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
) })}>
308 <Icon id
='check' className
='verified__mark' />
310 <a href
={proof
.get('profile_url')} target
='_blank' rel
='noopener noreferrer'><span dangerouslySetInnerHTML
={{ __html: ' '+proof
.get('provider_username') }} /></a>
314 {fields
.map((pair
, i
) => (
316 <dt dangerouslySetInnerHTML
={{ __html: pair
.get('name_emojified') }} title
={pair
.get('name')} className
='translate' />
318 <dd className
={`${pair.get('verified_at') ? 'verified' : ''} translate`} title
={pair
.get('value_plain')}>
319 {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') }} />
326 {account
.get('id') !== me
&& !suspended
&& <AccountNoteContainer account
={account
} />}
328 {account
.get('note').length
> 0 && account
.get('note') !== '<p></p>' && <div className
='account__header__content translate' dangerouslySetInnerHTML
={content
} />}
332 <div className
='account__header__extra__links'>
333 <NavLink isActive
={this.isStatusesPageActive
} activeClassName
='active' to
={`/accounts/${account.get('id')}`} title
={intl
.formatNumber(account
.get('statuses_count'))}>
335 value
={account
.get('statuses_count')}
336 renderer
={counterRenderer('statuses')}
340 <NavLink exact activeClassName
='active' to
={`/accounts/${account.get('id')}/following`} title
={intl
.formatNumber(account
.get('following_count'))}>
342 value
={account
.get('following_count')}
343 renderer
={counterRenderer('following')}
347 <NavLink exact activeClassName
='active' to
={`/accounts/${account.get('id')}/followers`} title
={intl
.formatNumber(account
.get('followers_count'))}>
349 value
={account
.get('followers_count')}
350 renderer
={counterRenderer('followers')}