diff options
Diffstat (limited to 'app/javascript/flavours/glitch/features/account')
5 files changed, 176 insertions, 19 deletions
diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index 23120d57e..ce0584124 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -4,8 +4,8 @@ import PropTypes from 'prop-types'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { NavLink } from 'react-router-dom'; import { injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { me, isStaff } from 'flavours/glitch/util/initial_state'; -import { profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links'; +import { me, isStaff } from 'flavours/glitch/initial_state'; +import { profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; import Icon from 'flavours/glitch/components/icon'; export default @injectIntl diff --git a/app/javascript/flavours/glitch/features/account/components/featured_tags.js b/app/javascript/flavours/glitch/features/account/components/featured_tags.js new file mode 100644 index 000000000..d646b08b2 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/components/featured_tags.js @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Hashtag from 'flavours/glitch/components/hashtag'; + +const messages = defineMessages({ + lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' }, + empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' }, +}); + +export default @injectIntl +class FeaturedTags extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + account: ImmutablePropTypes.map, + featuredTags: ImmutablePropTypes.list, + tagged: PropTypes.string, + intl: PropTypes.object.isRequired, + }; + + render () { + const { account, featuredTags, intl } = this.props; + + if (!account || account.get('suspended') || featuredTags.isEmpty()) { + return null; + } + + return ( + <div className='getting-started__trends'> + <h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4> + + {featuredTags.take(3).map(featuredTag => ( + <Hashtag + key={featuredTag.get('name')} + name={featuredTag.get('name')} + href={featuredTag.get('url')} + to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`} + uses={featuredTag.get('statuses_count')} + withGraph={false} + description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)} + /> + ))} + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 53170b7a6..93831b3e7 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -3,8 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { autoPlayGif, me } from 'flavours/glitch/util/initial_state'; -import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links'; +import { autoPlayGif, me, domain } from 'flavours/glitch/initial_state'; +import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/utils/backend_links'; import classNames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import IconButton from 'flavours/glitch/components/icon_button'; @@ -14,11 +14,12 @@ import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; +import { Helmet } from 'react-helmet'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, follow: { id: 'account.follow', defaultMessage: 'Follow' }, - cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' }, + cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -51,8 +52,18 @@ const messages = defineMessages({ 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}' }, add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' }, + languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, + openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, }); +const titleFromAccount = account => { + const displayName = account.get('display_name'); + const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${domain}` : account.get('acct'); + const prefix = displayName.trim().length === 0 ? account.get('username') : displayName; + + return `${prefix} (@${acct})`; +}; + const dateFormatOptions = { month: 'short', day: 'numeric', @@ -85,6 +96,9 @@ class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, onEditAccountNote: PropTypes.func.isRequired, + onChangeLanguages: PropTypes.func.isRequired, + onInteractionModal: PropTypes.func.isRequired, + onOpenAvatar: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, domain: PropTypes.string.isRequired, hidden: PropTypes.bool, @@ -120,8 +134,16 @@ class Header extends ImmutablePureComponent { } } + handleAvatarClick = e => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + this.props.onOpenAvatar(); + } + } + render () { const { account, hidden, intl, domain } = this.props; + const { signedIn } = this.context.identity; if (!account) { return null; @@ -129,7 +151,9 @@ class Header extends ImmutablePureComponent { const accountNote = account.getIn(['relationship', 'note']); - const suspended = account.get('suspended'); + const suspended = account.get('suspended'); + const isRemote = account.get('acct') !== account.get('username'); + const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null; let info = []; let actionBtn = ''; @@ -155,12 +179,12 @@ class Header extends ImmutablePureComponent { } if (me !== account.get('id')) { - if (!account.get('relationship')) { // Wait until the relationship is loaded + if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded actionBtn = ''; } else if (account.getIn(['relationship', 'requested'])) { 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} />; } else if (!account.getIn(['relationship', 'blocking'])) { - actionBtn = <Button 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} />; + actionBtn = <Button 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={signedIn ? this.props.onFollow : this.props.onInteractionModal} />; } else if (account.getIn(['relationship', 'blocking'])) { actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; } @@ -180,12 +204,17 @@ class Header extends ImmutablePureComponent { lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />; } - if (account.get('id') !== me && !suspended) { + if (signedIn && account.get('id') !== me && !suspended) { 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 (isRemote) { + menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); + menu.push(null); + } + if ('share' in navigator && !suspended) { menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); menu.push(null); @@ -207,7 +236,7 @@ class Header extends ImmutablePureComponent { 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 { + } else if (signedIn) { if (account.getIn(['relationship', 'following'])) { if (!account.getIn(['relationship', 'muting'])) { if (account.getIn(['relationship', 'showing_reblogs'])) { @@ -215,6 +244,9 @@ class Header extends ImmutablePureComponent { } else { menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle }); } + + menu.push({ text: intl.formatMessage(messages.languages), action: this.props.onChangeLanguages }); + menu.push(null); } menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); @@ -237,15 +269,13 @@ class Header extends ImmutablePureComponent { 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]; - + if (signedIn && isRemote) { menu.push(null); if (account.getIn(['relationship', 'domain_blocking'])) { - menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain }); + menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); } else { - menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain }); + menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain }); } } @@ -257,7 +287,9 @@ class Header extends ImmutablePureComponent { const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); - const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isLocal = account.get('acct').indexOf('@') === -1; + const acct = isLocal && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); + const isIndexable = !account.get('noindex'); let badge; @@ -281,7 +313,7 @@ class Header extends ImmutablePureComponent { <div className='account__header__bar'> <div className='account__header__tabs'> - <a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'> + <a className='avatar' href={account.get('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}> <Avatar account={suspended || hidden ? undefined : account} size={90} /> </a> @@ -296,7 +328,7 @@ class Header extends ImmutablePureComponent { </React.Fragment> )} - <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> + <DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' size={24} direction='right' /> </div> )} </div> @@ -308,7 +340,7 @@ class Header extends ImmutablePureComponent { </h1> </div> - <AccountNoteContainer account={account} /> + {signedIn && <AccountNoteContainer account={account} />} {!(suspended || hidden) && ( <div className='account__header__extra'> @@ -334,6 +366,11 @@ class Header extends ImmutablePureComponent { </div> )} </div> + + <Helmet> + <title>{titleFromAccount(account)}</title> + <meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} /> + </Helmet> </div> ); } diff --git a/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js new file mode 100644 index 000000000..6f0b06941 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/containers/featured_tags_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import FeaturedTags from '../components/featured_tags'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import { List as ImmutableList } from 'immutable'; + +const mapStateToProps = () => { + const getAccount = makeGetAccount(); + + return (state, { accountId }) => ({ + account: getAccount(state, accountId), + featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()), + }); +}; + +export default connect(mapStateToProps)(FeaturedTags); diff --git a/app/javascript/flavours/glitch/features/account/navigation.js b/app/javascript/flavours/glitch/features/account/navigation.js new file mode 100644 index 000000000..edae38ce5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/navigation.js @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import FeaturedTags from 'flavours/glitch/features/account/containers/featured_tags_container'; +import { normalizeForLookup } from 'flavours/glitch/reducers/accounts_map'; + +const mapStateToProps = (state, { match: { params: { acct } } }) => { + const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]); + + if (!accountId) { + return { + isLoading: true, + }; + } + + return { + accountId, + isLoading: false, + }; +}; + +export default @connect(mapStateToProps) +class AccountNavigation extends React.PureComponent { + + static propTypes = { + match: PropTypes.shape({ + params: PropTypes.shape({ + acct: PropTypes.string, + tagged: PropTypes.string, + }).isRequired, + }).isRequired, + + accountId: PropTypes.string, + isLoading: PropTypes.bool, + }; + + render () { + const { accountId, isLoading, match: { params: { tagged } } } = this.props; + + if (isLoading) { + return null; + } + + return ( + <> + <div className='flex-spacer' /> + <FeaturedTags accountId={accountId} tagged={tagged} /> + </> + ); + } + +} |