diff options
6 files changed, 356 insertions, 461 deletions
diff --git a/app/javascript/mastodon/features/account/components/action_bar.js b/app/javascript/mastodon/features/account/components/action_bar.js deleted file mode 100644 index 8ed4c917a..000000000 --- a/app/javascript/mastodon/features/account/components/action_bar.js +++ /dev/null @@ -1,190 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; -import { NavLink } from 'react-router-dom'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { me, isStaff } from '../../../initial_state'; -import { shortNumberFormat } from '../../../utils/numbers'; - -const messages = defineMessages({ - mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - report: { id: 'account.report', defaultMessage: 'Report @{name}' }, - share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, - media: { id: 'account.media', defaultMessage: 'Media' }, - blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, - unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, - hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, - showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, - pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, - lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, - blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, - mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, - endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, - unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, - add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, - admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, -}); - -export default @injectIntl -class ActionBar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onFollow: PropTypes.func, - onBlock: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onDirect: PropTypes.func.isRequired, - onReblogToggle: PropTypes.func.isRequired, - onReport: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onBlockDomain: PropTypes.func.isRequired, - onUnblockDomain: PropTypes.func.isRequired, - onEndorseToggle: PropTypes.func.isRequired, - onAddToList: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleShare = () => { - navigator.share({ - url: this.props.account.get('url'), - }); - } - - isStatusesPageActive = (match, location) => { - if (!match) { - return false; - } - return !location.pathname.match(/\/(followers|following)\/?$/); - } - - render () { - const { account, intl } = this.props; - - let menu = []; - let extraInfo = ''; - - if (account.get('id') !== me) { - menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); - menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); - menu.push(null); - } - - if ('share' in navigator) { - menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); - menu.push(null); - } - - if (account.get('id') === me) { - menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); - menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); - menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); - menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); - menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); - menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); - menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); - } else { - if (account.getIn(['relationship', 'following'])) { - if (account.getIn(['relationship', 'showing_reblogs'])) { - menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); - } else { - menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); - } - - menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); - menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList }); - menu.push(null); - } - - if (account.getIn(['relationship', 'muting'])) { - menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); - } else { - menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); - } - - if (account.getIn(['relationship', 'blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); - } else { - menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); - } - - menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); - } - - if (account.get('acct') !== account.get('username')) { - const domain = account.get('acct').split('@')[1]; - - extraInfo = ( - <div className='account__disclaimer'> - <FormattedMessage - id='account.disclaimer_full' - defaultMessage="Information below may reflect the user's profile incompletely." - /> - {' '} - <a target='_blank' rel='noopener' href={account.get('url')}> - <FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' /> - </a> - </div> - ); - - menu.push(null); - - if (account.getIn(['relationship', 'domain_blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); - } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); - } - } - - if (account.get('id') !== me && isStaff) { - menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); - } - - return ( - <div> - {extraInfo} - - <div className='account__action-bar'> - <div className='account__action-bar-links'> - <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> - <FormattedMessage id='account.posts' defaultMessage='Toots' /> - <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> - </NavLink> - - <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> - <FormattedMessage id='account.follows' defaultMessage='Follows' /> - <strong>{shortNumberFormat(account.get('following_count'))}</strong> - </NavLink> - - <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> - <FormattedMessage id='account.followers' defaultMessage='Followers' /> - <strong>{shortNumberFormat(account.get('followers_count'))}</strong> - </NavLink> - </div> - - <div className='account__action-bar-dropdown'> - <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> - </div> - </div> - </div> - ); - } - -} diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 7fe6d6a4f..41e394585 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -2,13 +2,15 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import IconButton from '../../../components/icon_button'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +import Button from 'mastodon/components/button'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me } from '../../../initial_state'; +import { autoPlayGif, me, isStaff } from 'mastodon/initial_state'; import classNames from 'classnames'; import Icon from 'mastodon/components/icon'; +import Avatar from 'mastodon/components/avatar'; +import { shortNumberFormat } from 'mastodon/utils/numbers'; +import { NavLink } from 'react-router-dom'; +import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -18,6 +20,32 @@ const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' }, account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' }, + mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, + direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + report: { id: 'account.report', defaultMessage: 'Report @{name}' }, + share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, + media: { id: 'account.media', defaultMessage: 'Media' }, + blockDomain: { id: 'account.block_domain', defaultMessage: 'Hide everything from {domain}' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, + hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' }, + showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, + pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, + endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, + unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, + add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, + admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, }); const dateFormatOptions = { @@ -29,54 +57,6 @@ const dateFormatOptions = { minute: '2-digit', }; -class Avatar extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - state = { - isHovered: false, - }; - - handleMouseOver = () => { - if (this.state.isHovered) return; - this.setState({ isHovered: true }); - } - - handleMouseOut = () => { - if (!this.state.isHovered) return; - this.setState({ isHovered: false }); - } - - render () { - const { account } = this.props; - const { isHovered } = this.state; - - return ( - <Motion defaultStyle={{ radius: 90 }} style={{ radius: spring(isHovered ? 30 : 90, { stiffness: 180, damping: 12 }) }}> - {({ radius }) => ( - <a - href={account.get('url')} - className='account__header__avatar' - role='presentation' - target='_blank' - rel='noopener' - style={{ borderRadius: `${radius}px`, backgroundImage: `url(${autoPlayGif || isHovered ? account.get('avatar') : account.get('avatar_static')})` }} - onMouseOver={this.handleMouseOver} - onMouseOut={this.handleMouseOut} - onFocus={this.handleMouseOver} - onBlur={this.handleMouseOut} - > - <span style={{ display: 'none' }}>{account.get('acct')}</span> - </a> - )} - </Motion> - ); - } - -} - export default @injectIntl class Header extends ImmutablePureComponent { @@ -85,64 +65,57 @@ class Header extends ImmutablePureComponent { onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + domain: PropTypes.string.isRequired, }; openEditProfile = () => { window.open('/settings/profile', '_blank'); } + isStatusesPageActive = (match, location) => { + if (!match) { + return false; + } + + return !location.pathname.match(/\/(followers|following)\/?$/); + } + render () { - const { account, intl } = this.props; + const { account, intl, domain } = this.props; if (!account) { return null; } - let info = ''; - let mutingInfo = ''; + let info = []; let actionBtn = ''; let lockedIcon = ''; + let menu = []; if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { - info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>; + info.push(<span className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>); } else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { - info = <span className='account--follows-info'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>; + info.push(<span className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>); } if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) { - mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>; + info.push(<span className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>); } else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) { - mutingInfo = <span className='account--muting-info'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>; + info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain hidden' /></span>); } if (me !== account.get('id')) { if (!account.get('relationship')) { // Wait until the relationship is loaded actionBtn = ''; } else if (account.getIn(['relationship', 'requested'])) { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} active icon='hourglass' title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} /> - </div> - ); + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />; } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} /> - </div> - ); + actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />; } else if (account.getIn(['relationship', 'blocking'])) { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} /> - </div> - ); + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; } } else { - actionBtn = ( - <div className='account--action-button'> - <IconButton size={26} icon='pencil' title={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} /> - </div> - ); + actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />; } if (account.get('moved') && !account.getIn(['relationship', 'following'])) { @@ -153,40 +126,145 @@ class Header extends ImmutablePureComponent { lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />; } + if (account.get('id') !== me) { + menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention }); + menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect }); + menu.push(null); + } + + if ('share' in navigator) { + menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); + menu.push(null); + } + + if (account.get('id') === me) { + menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); + menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); + menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); + menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); + menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); + menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); + menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); + } else { + if (account.getIn(['relationship', 'following'])) { + if (account.getIn(['relationship', 'showing_reblogs'])) { + menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } else { + menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); + } + + menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); + menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList }); + menu.push(null); + } + + if (account.getIn(['relationship', 'muting'])) { + menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute }); + } else { + menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute }); + } + + if (account.getIn(['relationship', 'blocking'])) { + menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock }); + } else { + menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock }); + } + + menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); + } + + if (account.get('acct') !== account.get('username')) { + const domain = account.get('acct').split('@')[1]; + + menu.push(null); + + if (account.getIn(['relationship', 'domain_blocking'])) { + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); + } else { + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); + } + } + + if (account.get('id') !== me && isStaff) { + menu.push(null); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); + } + const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const badge = account.get('bot') ? (<div className='roles'><div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div></div>) : null; + const badge = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null; + const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); return ( - <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${autoPlayGif ? account.get('header') : account.get('header_static')})` }}> - <div> - <Avatar account={account} /> + <div className={classNames('account__header', { inactive: !!account.get('moved') })}> + <div className='account__header__image'> + <div className='account__header__info'> + {info} + </div> - <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} /> - <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span> + <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' /> + </div> - {badge} + <div className='account__header__bar'> + <div className='account__header__tabs'> + <a className='avatar' href={account.get('url')}> + <Avatar account={account} size={90} /> + </a> - <div className='account__header__content' dangerouslySetInnerHTML={content} /> + <div className='spacer' /> - {fields.size > 0 && ( - <div className='account__header__fields'> - {fields.map((pair, i) => ( - <dl key={i}> - <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> + <div className='account__header__tabs__buttons'> + <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> - <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> - {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') }} /> - </dd> - </dl> - ))} + {actionBtn} </div> - )} + </div> + + <div className='account__header__tabs__name'> + <h1> + <span dangerouslySetInnerHTML={displayNameHtml} /> {badge} + <small>@{acct} {lockedIcon}</small> + </h1> + </div> + + <div className='account__header__extra'> + <div className='account__header__bio'> + {fields.size > 0 && ( + <div className='account__header__fields'> + {fields.map((pair, i) => ( + <dl key={i}> + <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> - {info} - {mutingInfo} - {actionBtn} + <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> + {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') }} /> + </dd> + </dl> + ))} + </div> + )} + + {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />} + </div> + + <div className='account__header__extra__links'> + <NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}> + <strong>{shortNumberFormat(account.get('statuses_count'))}</strong> <FormattedMessage id='account.posts' defaultMessage='Toots' /> + </NavLink> + + <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}> + <strong>{shortNumberFormat(account.get('following_count'))}</strong> <FormattedMessage id='account.follows' defaultMessage='Follows' /> + </NavLink> + + <NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> + <strong>{shortNumberFormat(account.get('followers_count'))}</strong> <FormattedMessage id='account.followers' defaultMessage='Followers' /> + </NavLink> + </div> + </div> </div> </div> ); diff --git a/app/javascript/mastodon/features/account_timeline/components/header.js b/app/javascript/mastodon/features/account_timeline/components/header.js index 779e116e0..7c621c090 100644 --- a/app/javascript/mastodon/features/account_timeline/components/header.js +++ b/app/javascript/mastodon/features/account_timeline/components/header.js @@ -2,7 +2,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import InnerHeader from '../../account/components/header'; -import ActionBar from '../../account/components/action_bar'; import MissingIndicator from '../../../components/missing_indicator'; import ImmutablePureComponent from 'react-immutable-pure-component'; import MovedNote from './moved_note'; @@ -25,6 +24,7 @@ export default class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, hideTabs: PropTypes.bool, + domain: PropTypes.string.isRequired, }; static contextTypes = { @@ -98,20 +98,7 @@ export default class Header extends ImmutablePureComponent { account={account} onFollow={this.handleFollow} onBlock={this.handleBlock} - /> - - <ActionBar - account={account} - onBlock={this.handleBlock} - onMention={this.handleMention} - onDirect={this.handleDirect} - onReblogToggle={this.handleReblogToggle} - onReport={this.handleReport} - onMute={this.handleMute} - onBlockDomain={this.handleBlockDomain} - onUnblockDomain={this.handleUnblockDomain} - onEndorseToggle={this.handleEndorseToggle} - onAddToList={this.handleAddToList} + domain={this.props.domain} /> {!hideTabs && ( diff --git a/app/javascript/mastodon/features/account_timeline/containers/header_container.js b/app/javascript/mastodon/features/account_timeline/containers/header_container.js index 0fd79d036..df742f9ec 100644 --- a/app/javascript/mastodon/features/account_timeline/containers/header_container.js +++ b/app/javascript/mastodon/features/account_timeline/containers/header_container.js @@ -33,6 +33,7 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { accountId }) => ({ account: getAccount(state, accountId), + domain: state.getIn(['meta', 'domain']), }); return mapStateToProps; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index caa45ee01..be48378f6 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1186,57 +1186,6 @@ a .account__avatar { white-space: nowrap; } -.account__header { - flex: 0 0 auto; - background: lighten($ui-base-color, 4%); - text-align: center; - background-size: cover; - background-position: center; - position: relative; - - &.inactive { - opacity: 0.5; - - .account__header__avatar { - filter: grayscale(100%); - } - - .account__header__username { - color: $secondary-text-color; - } - } - - & > div { - background: rgba(lighten($ui-base-color, 4%), 0.9); - padding: 20px 10px; - } - - .account__header__content { - color: $secondary-text-color; - } - - .account__header__display-name { - color: $primary-text-color; - display: inline-block; - width: 100%; - font-size: 20px; - line-height: 27px; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; - } - - .account__header__username { - color: $highlight-text-color; - font-size: 14px; - font-weight: 400; - display: block; - margin-bottom: 10px; - overflow: hidden; - text-overflow: ellipsis; - } -} - .account__disclaimer { padding: 10px; border-top: 1px solid lighten($ui-base-color, 8%); @@ -1265,39 +1214,6 @@ a .account__avatar { } } -.account__header__content { - color: $darker-text-color; - font-size: 14px; - font-weight: 400; - overflow: hidden; - word-break: normal; - word-wrap: break-word; - - p { - margin-bottom: 20px; - - &:last-child { - margin-bottom: 0; - } - } - - a { - color: inherit; - text-decoration: underline; - - &:hover { - text-decoration: none; - } - } -} - -.account__header__display-name { - .emojione { - width: 25px; - height: 25px; - } -} - .account__action-bar { border-top: 1px solid lighten($ui-base-color, 8%); border-bottom: 1px solid lighten($ui-base-color, 8%); @@ -1369,15 +1285,6 @@ a .account__avatar { } } -.account__header__avatar { - background-size: 90px 90px; - display: block; - height: 90px; - margin: 0 auto 10px; - overflow: hidden; - width: 90px; -} - .account-authorize { padding: 14px 10px; @@ -3154,29 +3061,11 @@ a.status-card.compact:hover { } } -.account--follows-info { +.relationship-tag { color: $primary-text-color; - position: absolute; - top: 10px; - left: 10px; + margin-bottom: 4px; opacity: 0.7; - display: inline-block; - vertical-align: top; - background-color: rgba($base-overlay-background, 0.4); - text-transform: uppercase; - font-size: 11px; - font-weight: 500; - padding: 4px; - border-radius: 4px; -} - -.account--muting-info { - color: $primary-text-color; - position: absolute; - top: 40px; - left: 10px; - opacity: 0.7; - display: inline-block; + display: block; vertical-align: top; background-color: rgba($base-overlay-background, 0.4); text-transform: uppercase; @@ -3186,12 +3075,6 @@ a.status-card.compact:hover { border-radius: 4px; } -.account--action-button { - position: absolute; - top: 10px; - right: 20px; -} - .setting-toggle { display: block; line-height: 24px; @@ -5348,53 +5231,188 @@ noscript { } } -.account__header .roles { - margin-top: 20px; - margin-bottom: 20px; - padding: 0 15px; +.account__header__content { + color: $darker-text-color; + font-size: 14px; + font-weight: 400; + overflow: hidden; + word-break: normal; + word-wrap: break-word; + + p { + margin-bottom: 20px; + + &:last-child { + margin-bottom: 0; + } + } + + a { + color: inherit; + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } } -.account__header .account__header__fields { - font-size: 14px; - line-height: 20px; +.account__header { overflow: hidden; - margin: 20px -10px -20px; - border-bottom: 0; - border-top: 0; - dl { - border-top: 1px solid lighten($ui-base-color, 4%); - border-bottom: 0; - display: flex; + &.inactive { + opacity: 0.5; + + .account__header__image, + .account__avatar { + filter: grayscale(100%); + } } - dt, - dd { - box-sizing: border-box; - padding: 14px 5px; - text-align: center; - max-height: 48px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; + &__info { + position: absolute; + top: 10px; + left: 10px; } - dt { - color: $darker-text-color; + &__image { + overflow: hidden; + height: 145px; + position: relative; background: darken($ui-base-color, 4%); - width: 120px; - flex: 0 0 auto; - font-weight: 500; + + img { + object-fit: cover; + display: block; + width: 100%; + height: 100%; + margin: 0; + } } - dd { - flex: 1 1 auto; - color: $primary-text-color; - background: $ui-base-color; + &__bar { + position: relative; + background: lighten($ui-base-color, 4%); + padding: 5px; + border-bottom: 1px solid lighten($ui-base-color, 12%); - &.verified { - border: 1px solid rgba($valid-value-color, 0.5); - background: rgba($valid-value-color, 0.25); + .avatar { + display: block; + flex: 0 0 auto; + width: 90px; + margin-left: -2px; + + .account__avatar { + border: 2px solid lighten($ui-base-color, 4%); + } + } + } + + &__tabs { + display: flex; + align-items: flex-start; + padding: 7px 5px; + margin-top: -55px; + + &__buttons { + display: flex; + align-items: center; + padding-top: 55px; + + .icon-button { + border: 1px solid lighten($ui-base-color, 12%); + border-radius: 4px; + box-sizing: content-box; + padding: 2px; + margin: 0 8px; + } + } + + &__name { + padding: 5px; + + .account-role { + vertical-align: top; + } + + .emojione { + width: 22px; + height: 22px; + } + + h1 { + font-size: 16px; + line-height: 24px; + color: $primary-text-color; + font-weight: 500; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + small { + display: block; + font-size: 14px; + color: $darker-text-color; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + } + } + } + + .spacer { + flex: 1 1 auto; + } + } + + &__bio { + overflow: hidden; + margin: 0 -5px; + + .account__header__content { + padding: 20px 15px; + padding-bottom: 5px; + color: $primary-text-color; + } + + .account__header__fields { + margin: 0; + border-top: 1px solid lighten($ui-base-color, 12%); + + a { + color: lighten($ui-highlight-color, 8%); + } + + dl:first-child .verified { + border-radius: 0 4px 0 0; + } + + .verified a { + color: $valid-value-color; + } + } + } + + &__extra { + margin-top: 4px; + + &__links { + font-size: 14px; + color: $darker-text-color; + + a { + display: inline-block; + color: $darker-text-color; + text-decoration: none; + padding: 10px; + padding-top: 20px; + font-weight: 500; + + strong { + font-weight: 700; + color: $primary-text-color; + } + } } } } diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index a98fa52c4..2b1d988f2 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -677,6 +677,7 @@ color: $darker-text-color; text-decoration: none; padding: 15px; + font-weight: 500; strong { font-weight: 700; |