diff options
Diffstat (limited to 'app/javascript/flavours/glitch')
36 files changed, 630 insertions, 555 deletions
diff --git a/app/javascript/flavours/glitch/actions/identity_proofs.js b/app/javascript/flavours/glitch/actions/identity_proofs.js new file mode 100644 index 000000000..a7241da20 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/identity_proofs.js @@ -0,0 +1,30 @@ +import api from 'flavours/glitch/util/api'; + +export const IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST = 'IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS = 'IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS'; +export const IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL = 'IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL'; + +export const fetchAccountIdentityProofs = accountId => (dispatch, getState) => { + dispatch(fetchAccountIdentityProofsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/identity_proofs`) + .then(({ data }) => dispatch(fetchAccountIdentityProofsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountIdentityProofsFail(accountId, err))); +}; + +export const fetchAccountIdentityProofsRequest = id => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + id, +}); + +export const fetchAccountIdentityProofsSuccess = (accountId, identity_proofs) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + accountId, + identity_proofs, +}); + +export const fetchAccountIdentityProofsFail = (accountId, err) => ({ + type: IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, + accountId, + err, +}); diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index bc094eed5..b2d24e10b 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -37,6 +37,7 @@ export function submitSearch() { params: { q: value, resolve: true, + limit: 10, }, }).then(response => { if (response.data.accounts) { diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 1d3130604..e0cc652d2 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -146,7 +146,7 @@ export default class StatusActionBar extends ImmutablePureComponent { } handleBlockClick = () => { - this.props.onBlock(this.props.status.get('account')); + this.props.onBlock(this.props.status); } handleOpen = () => { diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 98a34ebaf..a62844185 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -198,7 +198,7 @@ export default class StatusContent extends React.PureComponent { <p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }} > - <span dangerouslySetInnerHTML={spoilerContent} /> + <span dangerouslySetInnerHTML={spoilerContent} lang={status.get('language')} /> {' '} <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> {toggleText} @@ -213,6 +213,7 @@ export default class StatusContent extends React.PureComponent { style={directionStyle} tabIndex={!hidden ? 0 : null} dangerouslySetInnerHTML={content} + lang={status.get('language')} /> {media} </div> @@ -231,6 +232,7 @@ export default class StatusContent extends React.PureComponent { <div ref={this.setRef} dangerouslySetInnerHTML={content} + lang={status.get('language')} tabIndex='0' /> {media} @@ -243,7 +245,7 @@ export default class StatusContent extends React.PureComponent { style={directionStyle} tabIndex='0' > - <div ref={this.setRef} dangerouslySetInnerHTML={content} tabIndex='0' /> + <div ref={this.setRef} dangerouslySetInnerHTML={content} lang={status.get('language')} tabIndex='0' /> {media} </div> ); diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index f783878b0..60636feb4 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -35,6 +35,7 @@ const messages = defineMessages({ blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); const makeMapStateToProps = () => { @@ -169,11 +170,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(openModal('VIDEO', { media, time })); }, - onBlock (account) { + onBlock (status) { + const account = status.get('account'); dispatch(openModal('CONFIRM', { message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, confirm: intl.formatMessage(messages.blockConfirm), onConfirm: () => dispatch(blockAccount(account.get('id'))), + secondary: intl.formatMessage(messages.blockAndReport), + onSecondary: () => { + dispatch(blockAccount(account.get('id'))); + dispatch(initReport(account, status)); + }, })); }, 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 fdacb7298..a2c00c1c2 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -3,58 +3,19 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { NavLink } from 'react-router-dom'; -import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +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'; - -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}' }, - 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}' }, -}); +import Icon from 'flavours/glitch/components/icon'; @injectIntl export default 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; @@ -65,56 +26,12 @@ export default class ActionBar extends React.PureComponent { render () { const { account, intl } = this.props; - let menu = []; let extraInfo = ''; - 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 }); - - 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) { - if (profileLink !== undefined) { - menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); - } - } 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 + <Icon icon='info-circle' fixedWidth /> <FormattedMessage id='account.disclaimer_full' defaultMessage="Information below may reflect the user's profile incompletely." /> @@ -124,22 +41,6 @@ export default class ActionBar extends React.PureComponent { </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 && (accountAdminLink !== undefined)) { - menu.push(null); - menu.push({ - text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), - href: accountAdminLink(account.get('id')), - }); } return ( @@ -147,10 +48,6 @@ export default class ActionBar extends React.PureComponent { {extraInfo} <div className='account__action-bar'> - <div className='account__action-bar-dropdown'> - <DropdownMenuContainer items={menu} icon='bars' size={24} direction='right' /> - </div> - <div className='account__action-bar-links'> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> <FormattedMessage id='account.posts' defaultMessage='Posts' /> diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 96696c2a5..13f7741c8 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -3,12 +3,14 @@ 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 Avatar from 'flavours/glitch/components/avatar'; -import IconButton from 'flavours/glitch/components/icon_button'; - -import { autoPlayGif, me } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, me, isStaff } from 'flavours/glitch/util/initial_state'; import classNames from 'classnames'; +import Icon from 'flavours/glitch/components/icon'; +import Avatar from 'flavours/glitch/components/avatar'; +import Button from 'flavours/glitch/components/button'; +import { shortNumberFormat } from 'flavours/glitch/util/numbers'; +import { NavLink } from 'react-router-dom'; +import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; const messages = defineMessages({ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, @@ -16,7 +18,34 @@ const messages = defineMessages({ 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' }, - link_verified_on: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' }, + 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 = { @@ -28,14 +57,16 @@ const dateFormatOptions = { minute: '2-digit', }; -@injectIntl -export default class Header extends ImmutablePureComponent { +export default @injectIntl +class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, + identity_props: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + domain: PropTypes.string.isRequired, }; openEditProfile = () => { @@ -43,109 +74,191 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account, intl } = this.props; + const { account, intl, domain, identity_proofs } = this.props; if (!account) { return null; } - let displayName = account.get('display_name_html'); - let fields = account.get('fields'); - let badge = account.get('bot') ? (<div className='roles'><div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div></div>) : 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'])) { actionBtn = ''; } - const content = { __html: account.get('note_emojified') }; + if (account.get('locked')) { + lockedIcon = <Icon icon='lock' title={intl.formatMessage(messages.account_locked)} />; + } - return ( - <div className='account__header__wrapper'> - <div className={classNames('account__header', { inactive: !!account.get('moved') })} style={{ backgroundImage: `url(${autoPlayGif ? account.get('header') : account.get('header_static')})` }}> - <div> - <a - href={account.get('url')} - className='account__header__avatar' - role='presentation' - target='_blank' - rel='noopener' - > - <Avatar account={account} size={90} /> - </a> + 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]; - <span className='account__header__display-name' dangerouslySetInnerHTML={{ __html: displayName }} /> - <span className='account__header__username'>@{account.get('acct')} {account.get('locked') ? <i className='fa fa-lock' /> : null}</span> + menu.push(null); - {badge} + 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 }); + } + } - <div className='account__header__content' dangerouslySetInnerHTML={content} /> + 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')}` }); + } - {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')} /> - <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> - {pair.get('verified_at') && <span title={intl.formatMessage(messages.link_verified_on, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> - </dd> - </dl> - ))} - </div> - )} + 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='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') })}> + <div className='account__header__image'> + <div className='account__header__info'> {info} - {mutingInfo} - {actionBtn} </div> + + <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' /> </div> - </div> + + <div className='account__header__bar'> + <div className='account__header__tabs'> + <a className='avatar' href={account.get('url')} rel='noopener' target='_blank'> + <Avatar account={account} size={90} /> + </a> + + <div className='spacer' /> + + <div className='account__header__tabs__buttons'> + {actionBtn} + + <DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' /> + </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 || identity_proofs.size > 0) && ( + <div className='account__header__fields'> + {identity_proofs.map((proof, i) => ( + <dl key={i}> + <dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} /> + + <dd className='verified'> + <a href={proof.get('proof_url')} target='_blank' rel='noopener'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}> + <Icon id='check' className='verified__mark' /> + </span></a> + <a href={proof.get('profile_url')} target='_blank' rel='noopener'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a> + </dd> + </dl> + ))} + {fields.map((pair, i) => ( + <dl key={i}> + <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> + + <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> + </div> + </div> ); } diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index 8dc0be93e..96cabe847 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -13,6 +13,7 @@ export default class Header extends ImmutablePureComponent { static propTypes = { account: ImmutablePropTypes.map, + identity_proofs: ImmutablePropTypes.list, onFollow: PropTypes.func.isRequired, onBlock: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, @@ -25,6 +26,7 @@ export default class Header extends ImmutablePureComponent { onEndorseToggle: PropTypes.func.isRequired, onAddToList: PropTypes.func.isRequired, hideTabs: PropTypes.bool, + domain: PropTypes.string.isRequired, }; static contextTypes = { @@ -84,7 +86,7 @@ export default class Header extends ImmutablePureComponent { } render () { - const { account, hideTabs } = this.props; + const { account, hideTabs, identity_proofs } = this.props; if (account === null) { return <MissingIndicator />; @@ -96,13 +98,9 @@ export default class Header extends ImmutablePureComponent { <InnerHeader account={account} + identity_proofs={identity_proofs} onFollow={this.handleFollow} onBlock={this.handleBlock} - /> - - <ActionBar - account={account} - onBlock={this.handleBlock} onMention={this.handleMention} onDirect={this.handleDirect} onReblogToggle={this.handleReblogToggle} @@ -112,6 +110,11 @@ export default class Header extends ImmutablePureComponent { onUnblockDomain={this.handleUnblockDomain} onEndorseToggle={this.handleEndorseToggle} onAddToList={this.handleAddToList} + domain={this.props.domain} + /> + + <ActionBar + account={account} /> {!hideTabs && ( diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index e333c31a1..787a36658 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -21,11 +21,13 @@ import { openModal } from 'flavours/glitch/actions/modal'; import { blockDomain, unblockDomain } from 'flavours/glitch/actions/domain_blocks'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { unfollowModal } from 'flavours/glitch/util/initial_state'; +import { List as ImmutableList } from 'immutable'; const messages = defineMessages({ unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' }, blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, + blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); const makeMapStateToProps = () => { @@ -33,6 +35,8 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, { accountId }) => ({ account: getAccount(state, accountId), + domain: state.getIn(['meta', 'domain']), + identity_proofs: state.getIn(['identity_proofs', accountId], ImmutableList()), }); return mapStateToProps; @@ -64,6 +68,11 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, confirm: intl.formatMessage(messages.blockConfirm), onConfirm: () => dispatch(blockAccount(account.get('id'))), + secondary: intl.formatMessage(messages.blockAndReport), + onSecondary: () => { + dispatch(blockAccount(account.get('id'))); + dispatch(initReport(account)); + }, })); } }, diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index d683b8789..9971c0f4a 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -12,6 +12,7 @@ import HeaderContainer from './containers/header_container'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; +import { fetchAccountIdentityProofs } from '../../actions/identity_proofs'; const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { const path = withReplies ? `${accountId}:with_replies` : accountId; @@ -41,6 +42,7 @@ export default class AccountTimeline extends ImmutablePureComponent { const { params: { accountId }, withReplies } = this.props; this.props.dispatch(fetchAccount(accountId)); + this.props.dispatch(fetchAccountIdentityProofs(accountId)); if (!withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } @@ -50,6 +52,7 @@ export default class AccountTimeline extends ImmutablePureComponent { componentWillReceiveProps (nextProps) { if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); + this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); if (!nextProps.withReplies) { this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js index ac7a14ef4..4574c0e1e 100644 --- a/app/javascript/flavours/glitch/features/drawer/results/index.js +++ b/app/javascript/flavours/glitch/features/drawer/results/index.js @@ -10,6 +10,7 @@ import spring from 'react-motion/lib/spring'; import { Link } from 'react-router-dom'; // Components. +import Icon from 'flavours/glitch/components/icon'; import AccountContainer from 'flavours/glitch/containers/account_container'; import StatusContainer from 'flavours/glitch/containers/status_container'; import Hashtag from 'flavours/glitch/components/hashtag'; @@ -62,6 +63,7 @@ export default function DrawerResults ({ }} > <header> + <Icon icon='search' fixedWidth /> <FormattedMessage {...messages.total} values={{ count }} @@ -69,7 +71,7 @@ export default function DrawerResults ({ </header> {accounts && accounts.size ? ( <section> - <h5><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> + <h5><Icon icon='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5> {accounts.map( accountId => ( @@ -83,7 +85,7 @@ export default function DrawerResults ({ ) : null} {statuses && statuses.size ? ( <section> - <h5><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> + <h5><Icon icon='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5> {statuses.map( statusId => ( @@ -97,7 +99,7 @@ export default function DrawerResults ({ ) : null} {hashtags && hashtags.size ? ( <section> - <h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> + <h5><Icon icon='hashtag' fixedWidth /><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> {hashtags.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} </section> diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index d963039dc..a78117971 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -129,7 +129,6 @@ class ModifierPickerMenu extends React.PureComponent { active: PropTypes.bool, onSelect: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, - modifier: PropTypes.number, }; handleClick = e => { @@ -166,36 +165,20 @@ class ModifierPickerMenu extends React.PureComponent { setRef = c => { this.node = c; - if (this.node) { - this.node.querySelector('li:first-child button').focus(); // focus the first element when opened - } } render () { - const { active, modifier } = this.props; + const { active } = this.props; return ( - <ul - className='emoji-picker-dropdown__modifiers__menu' - style={{ display: active ? 'block' : 'none' }} - role='menuitem' - ref={this.setRef} - > - {[1, 2, 3, 4, 5, 6].map(i => ( - <li - onClick={this.handleClick} - role='menuitemradio' - aria-checked={i === (modifier || 1)} - data-index={i} - key={i} - > - <Emoji - emoji='fist' set='twitter' size={22} sheetSize={32} skin={i} - backgroundImageFn={backgroundImageFn} - /> - </li> - ))} - </ul> + <div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}> + <button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} /></button> + <button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} /></button> + </div> ); } @@ -227,22 +210,10 @@ class ModifierPicker extends React.PureComponent { render () { const { active, modifier } = this.props; - function setRef(ref) { - if (!ref) { - return; - } - // TODO: It would be nice if we could pass props directly to emoji-mart's buttons. - const button = ref.querySelector('button'); - button.setAttribute('aria-haspopup', 'true'); - button.setAttribute('aria-expanded', active); - } - return ( <div className='emoji-picker-dropdown__modifiers'> - <div ref={setRef}> - <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> - </div> - <ModifierPickerMenu active={active} modifier={modifier} onSelect={this.handleSelect} onClose={this.props.onClose} /> + <Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} /> + <ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} /> </div> ); } diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 66cc10d78..8291319c3 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -98,7 +98,7 @@ export default class ActionBar extends React.PureComponent { } handleBlockClick = () => { - this.props.onBlock(this.props.status.get('account')); + this.props.onBlock(this.props.status); } handleReport = () => { diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index 6d3909ea7..e6c390537 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -38,6 +38,7 @@ const messages = defineMessages({ blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, }); const makeMapStateToProps = () => { @@ -136,11 +137,17 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(openModal('VIDEO', { media, time })); }, - onBlock (account) { + onBlock (status) { + const account = status.get('account'); dispatch(openModal('CONFIRM', { message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, confirm: intl.formatMessage(messages.blockConfirm), onConfirm: () => dispatch(blockAccount(account.get('id'))), + secondary: intl.formatMessage(messages.blockAndReport), + onSecondary: () => { + dispatch(blockAccount(account.get('id'))); + dispatch(initReport(account, status)); + }, })); }, diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 73d3c7e9e..7f8f02188 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -54,6 +54,7 @@ const messages = defineMessages({ detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' }, tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' }, }); @@ -279,13 +280,19 @@ export default class Status extends ImmutablePureComponent { this.setState({ isExpanded: !isExpanded, threadExpanded: !isExpanded }); } - handleBlockClick = (account) => { + handleBlockClick = (status) => { const { dispatch, intl } = this.props; + const account = status.get('account'); dispatch(openModal('CONFIRM', { message: <FormattedMessage id='confirmations.block.message' defaultMessage='Are you sure you want to block {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />, confirm: intl.formatMessage(messages.blockConfirm), onConfirm: () => dispatch(blockAccount(account.get('id'))), + secondary: intl.formatMessage(messages.blockAndReport), + onSecondary: () => { + dispatch(blockAccount(account.get('id'))); + dispatch(initReport(account, status)); + }, })); } diff --git a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js index 07281f818..970df30b6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js @@ -11,6 +11,8 @@ export default class ConfirmationModal extends React.PureComponent { confirm: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired, + secondary: PropTypes.string, + onSecondary: PropTypes.func, onDoNotAsk: PropTypes.func, intl: PropTypes.object.isRequired, }; @@ -27,6 +29,11 @@ export default class ConfirmationModal extends React.PureComponent { } } + handleSecondary = () => { + this.props.onClose(); + this.props.onSecondary(); + } + handleCancel = () => { this.props.onClose(); } @@ -40,7 +47,7 @@ export default class ConfirmationModal extends React.PureComponent { } render () { - const { message, confirm, onDoNotAsk } = this.props; + const { message, confirm, secondary, onDoNotAsk } = this.props; return ( <div className='modal-root__modal confirmation-modal'> @@ -61,6 +68,9 @@ export default class ConfirmationModal extends React.PureComponent { <Button onClick={this.handleCancel} className='confirmation-modal__cancel-button'> <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' /> </Button> + {secondary !== undefined && ( + <Button text={secondary} onClick={this.handleSecondary} className='confirmation-modal__secondary-button' /> + )} <Button text={confirm} onClick={this.handleClick} ref={this.setRef} /> </div> </div> diff --git a/app/javascript/flavours/glitch/reducers/identity_proofs.js b/app/javascript/flavours/glitch/reducers/identity_proofs.js new file mode 100644 index 000000000..58af0a5fa --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/identity_proofs.js @@ -0,0 +1,25 @@ +import { Map as ImmutableMap, fromJS } from 'immutable'; +import { + IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST, + IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS, + IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL, +} from '../actions/identity_proofs'; + +const initialState = ImmutableMap(); + +export default function identityProofsReducer(state = initialState, action) { + switch(action.type) { + case IDENTITY_PROOFS_ACCOUNT_FETCH_REQUEST: + return state.set('isLoading', true); + case IDENTITY_PROOFS_ACCOUNT_FETCH_FAIL: + return state.set('isLoading', false); + case IDENTITY_PROOFS_ACCOUNT_FETCH_SUCCESS: + return state.update(identity_proofs => identity_proofs.withMutations(map => { + map.set('isLoading', false); + map.set('loaded', true); + map.set(action.accountId, fromJS(action.identity_proofs)); + })); + default: + return state; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 7b3e0f651..76b38adb4 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -30,6 +30,7 @@ import listAdder from './list_adder'; import filters from './filters'; import pinnedAccountsEditor from './pinned_accounts_editor'; import polls from './polls'; +import identity_proofs from './identity_proofs'; const reducers = { dropdown_menu, @@ -57,6 +58,7 @@ const reducers = { notifications, height_cache, custom_emojis, + identity_proofs, lists, listEditor, listAdder, diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index ca71c3833..cb233de1c 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -73,14 +73,15 @@ const updateTimeline = (state, timeline, status) => { })); }; -const deleteStatus = (state, id, accountId, references) => { +const deleteStatus = (state, id, accountId, references, exclude_account = null) => { state.keySeq().forEach(timeline => { - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) + state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); }); // Remove reblogs of deleted status references.forEach(ref => { - state = deleteStatus(state, ref[0], ref[1], []); + state = deleteStatus(state, ref[0], ref[1], [], exclude_account); }); return state; @@ -99,7 +100,7 @@ const filterTimelines = (state, relationship, statuses) => { } references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); - state = deleteStatus(state, status.get('id'), status.get('account'), references); + state = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id); }); return state; diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss index 586802185..d542b1083 100644 --- a/app/javascript/flavours/glitch/styles/_mixins.scss +++ b/app/javascript/flavours/glitch/styles/_mixins.scss @@ -1,6 +1,5 @@ @mixin avatar-radius() { border-radius: $ui-avatar-border-size; - background: transparent no-repeat; background-position: 50%; background-clip: padding-box; } diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 42f53f525..05c7821e4 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -220,6 +220,11 @@ $content-width: 840px; color: $error-value-color; font-weight: 500; } + + .neutral-hint { + color: $dark-text-color; + font-weight: 500; + } } @media screen and (max-width: $no-columns-breakpoint) { diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 0b7b58bb0..00380c575 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -79,68 +79,8 @@ background: lighten($ui-base-color, 4%); } -.account__header { - flex: 0 0 auto; - background: lighten($ui-base-color, 4%); - text-align: center; - background-size: cover; - background-position: center; - position: relative; - - .account__avatar { - @include avatar-radius(); - @include avatar-size(90px); - display: block; - margin: 0 auto 10px; - overflow: hidden; - } - - &.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%); color: $dark-text-color; strong { @@ -166,39 +106,6 @@ } } -.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%); @@ -208,24 +115,6 @@ display: flex; } -.account__action-bar-dropdown { - padding: 10px; - - .dropdown--active { - .dropdown__content.dropdown__right { - left: 6px; - right: initial; - } - - &::after { - bottom: initial; - margin-left: 11px; - margin-top: -7px; - right: initial; - } - } -} - .account__action-bar-links { display: flex; flex: 1 1 auto; @@ -241,6 +130,10 @@ padding: 10px 0; border-bottom: 4px solid transparent; + &:first-child { + border-left: 0; + } + &.active { border-bottom: 4px solid $ui-highlight-color; } @@ -270,15 +163,6 @@ } } -.account__header__avatar { - background-size: 90px 90px; - display: block; - height: 90px; - margin: 0 auto 10px; - overflow: hidden; - width: 90px; -} - .account-authorize { padding: 14px 10px; @@ -427,42 +311,22 @@ } } -.account--follows-info { +.relationship-tag { color: $primary-text-color; - position: absolute; - top: 10px; - left: 10px; - opacity: 0.7; - display: inline-block; + margin-bottom: 4px; + display: block; vertical-align: top; - background-color: rgba($base-overlay-background, 0.4); + background-color: $base-overlay-background; 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; - 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--action-button { - position: absolute; - top: 10px; - right: 20px; + &:hover { + opacity: 1; + } } .account-gallery__container { @@ -614,8 +478,193 @@ } } -.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 { + overflow: hidden; + + &.inactive { + opacity: 0.5; + + .account__header__image, + .account__avatar { + filter: grayscale(100%); + } + } + + &__info { + position: absolute; + top: 10px; + left: 10px; + } + + &__image { + overflow: hidden; + height: 145px; + position: relative; + background: darken($ui-base-color, 4%); + + img { + object-fit: cover; + display: block; + width: 100%; + height: 100%; + margin: 0; + } + } + + &__bar { + position: relative; + background: lighten($ui-base-color, 4%); + padding: 5px; + border-bottom: 1px solid lighten($ui-base-color, 12%); + + .avatar { + display: block; + flex: 0 0 auto; + width: 94px; + margin-left: -2px; + + .account__avatar { + background: darken($ui-base-color, 8%); + 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; + overflow: hidden; + + .icon-button { + border: 1px solid lighten($ui-base-color, 12%); + border-radius: 4px; + box-sizing: content-box; + padding: 2px; + } + + .button { + 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/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index f4931c36c..d22783b94 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -196,43 +196,35 @@ overflow-y: auto; & > header { - border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; color: $dark-text-color; background: lighten($ui-base-color, 2%); - font-size: 14px; + padding: 15px; font-weight: 500; + font-size: 16px; + cursor: default; + + .fa { + display: inline-block; + margin-right: 5px; + } } & > section { - background: $ui-base-color; - margin-bottom: 20px; + margin-bottom: 5px; h5 { - position: relative; - - &::before { - content: ""; - display: block; - position: absolute; - left: 0; - right: 0; - top: 50%; - width: 100%; - height: 0; - border-top: 1px solid lighten($ui-base-color, 8%); - } + background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); + cursor: default; + display: flex; + padding: 15px; + font-weight: 500; + font-size: 16px; + color: $dark-text-color; - span { + .fa { display: inline-block; - background: $ui-base-color; - color: $darker-text-color; - font-size: 14px; - font-weight: 500; - padding: 10px; - position: relative; - z-index: 1; - cursor: default; + margin-right: 5px; } } diff --git a/app/javascript/flavours/glitch/styles/components/emoji.scss b/app/javascript/flavours/glitch/styles/components/emoji.scss index ccfd42f28..dd386d698 100644 --- a/app/javascript/flavours/glitch/styles/components/emoji.scss +++ b/app/javascript/flavours/glitch/styles/components/emoji.scss @@ -44,11 +44,11 @@ box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2); overflow: hidden; - li { + button { display: block; cursor: pointer; border: 0; - padding: 3px 8px; + padding: 4px 8px; background: transparent; &:hover, diff --git a/app/javascript/flavours/glitch/styles/components/emoji_picker.scss b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss index 171623352..dcc551c5b 100644 --- a/app/javascript/flavours/glitch/styles/components/emoji_picker.scss +++ b/app/javascript/flavours/glitch/styles/components/emoji_picker.scss @@ -1,5 +1,3 @@ -@import '~emoji-mart/css/emoji-mart.css'; - .emoji-mart { &, * { @@ -53,14 +51,6 @@ &:hover { color: darken($lighter-text-color, 4%); - - svg { - fill: darken($lighter-text-color, 4%); - } - } - - svg { - fill: $lighter-text-color; } } @@ -69,19 +59,11 @@ &:hover { color: darken($highlight-text-color, 4%); - - svg { - fill: darken($highlight-text-color, 4%); - } } .emoji-mart-anchor-bar { bottom: 0; } - - svg { - fill: $highlight-text-color; - } } .emoji-mart-anchor-bar { @@ -101,6 +83,7 @@ } svg { + fill: currentColor; max-height: 18px; } } @@ -120,14 +103,15 @@ } .emoji-mart-search { - margin: 10px 40px 10px 5px; + padding: 10px; + padding-right: 45px; background: $simple-background-color; input { font-size: 14px; font-weight: 400; padding: 7px 9px; - font-family: $font-sans-serif; + font-family: inherit; display: block; width: 100%; background: rgba($ui-secondary-color, 0.3); @@ -182,7 +166,6 @@ font-weight: 500; padding: 5px 6px; background: $simple-background-color; - font-family: $font-sans-serif; } } diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index b9811f25c..b098676b0 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -35,6 +35,17 @@ transition: all 200ms ease-out; } + &--destructive { + transition: none; + + &:active, + &:focus, + &:hover { + background-color: $error-red; + transition: none; + } + } + &:disabled { background-color: $ui-primary-color; cursor: default; @@ -269,9 +280,7 @@ .display-name { display: block; - padding: 6px 0; max-width: 100%; - height: 36px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -1263,7 +1272,6 @@ noscript { @import 'domains'; @import 'status'; @import 'modal'; -@import 'metadata'; @import 'composer'; @import 'columns'; @import 'regeneration_indicator'; diff --git a/app/javascript/flavours/glitch/styles/components/metadata.scss b/app/javascript/flavours/glitch/styles/components/metadata.scss index da045574a..e69de29bb 100644 --- a/app/javascript/flavours/glitch/styles/components/metadata.scss +++ b/app/javascript/flavours/glitch/styles/components/metadata.scss @@ -1,45 +0,0 @@ -.account__header .account__header__fields { - font-size: 15px; - line-height: 20px; - overflow: hidden; - margin: 20px -10px -20px; - border-bottom: 0; - border-top: 0; - - dl { - background: $ui-base-color; - border-top: 1px solid lighten($ui-base-color, 4%); - border-bottom: 0; - display: flex; - } - - dt, - dd { - box-sizing: border-box; - padding: 14px 5px; - text-align: center; - max-height: 48px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - dt { - color: $darker-text-color; - background: lighten($ui-base-color, 13%); - width: 120px; - flex: 0 0 auto; - font-weight: 500; - } - - dd { - flex: 1 1 auto; - color: $primary-text-color; - background: $ui-base-color; - - &.verified { - border: 1px solid rgba($valid-value-color, 0.5); - background: rgba($valid-value-color, 0.25); - } - } -} diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index 3598959e7..fece8593b 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -671,6 +671,7 @@ .confirmation-modal__action-bar, .mute-modal__action-bar { + .confirmation-modal__secondary-button, .confirmation-modal__cancel-button, .mute-modal__cancel-button { background-color: transparent; diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index 9d2757065..b73dd3d09 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -357,6 +357,7 @@ .status__info__account { display: flex; + align-items: center; } .status-check-box { diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index fd334f869..b27524739 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -10,12 +10,10 @@ } .logo-container { - margin: 100px auto; - margin-bottom: 50px; + margin: 100px auto 50px; - @media screen and (max-width: 400px) { - margin: 30px auto; - margin-bottom: 20px; + @media screen and (max-width: 500px) { + margin: 40px auto 0; } h1 { @@ -683,6 +681,7 @@ color: $darker-text-color; text-decoration: none; padding: 15px; + font-weight: 500; strong { font-weight: 700; diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 9ef45e425..91888d305 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -475,6 +475,42 @@ code { } } } + + &__overlay-area { + position: relative; + + &__overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background: rgba($ui-base-color, 0.65); + backdrop-filter: blur(2px); + border-radius: 4px; + + &__content { + text-align: center; + + &.rich-formatting { + &, + p { + color: $primary-text-color; + } + } + } + } + } +} + +.block-icon { + display: block; + margin: 0 auto; + margin-bottom: 10px; + font-size: 24px; } .flash-message { @@ -818,13 +854,19 @@ code { flex: 1; flex-direction: column; flex-shrink: 1; + max-width: 50%; &-sep { + align-self: center; flex-grow: 0; overflow: visible; position: relative; z-index: 1; } + + p { + word-break: break-word; + } } .account__avatar { @@ -846,12 +888,13 @@ code { height: 100%; left: 50%; position: absolute; + top: 0; width: 1px; } } &__row { - align-items: center; + align-items: flex-start; display: flex; flex-direction: row; } diff --git a/app/javascript/flavours/glitch/styles/metadata.scss b/app/javascript/flavours/glitch/styles/metadata.scss deleted file mode 100644 index 280848959..000000000 --- a/app/javascript/flavours/glitch/styles/metadata.scss +++ /dev/null @@ -1,56 +0,0 @@ -.account__header__fields { - $meta-table-border: lighten($ui-base-color, 8%); - padding: 0; - margin: 15px -15px -15px -15px; - border: 0 none; - border-top: 1px solid $meta-table-border; - border-bottom: 1px solid $meta-table-border; - font-size: 14px; - line-height: 20px; - - dl { - display: flex; - border-bottom: 1px solid $meta-table-border; - } - - dt, - dd { - box-sizing: border-box; - padding: 14px; - text-align: center; - max-height: 48px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - dt { - padding-left: 15px; - font-weight: 500; - text-align: center; - width: 120px; - flex: 0 0 auto; - color: $secondary-text-color; - background: darken($ui-base-color, 8%); - } - - dd { - flex: 1 1 auto; - color: $darker-text-color; - } - - a { - color: $highlight-text-color; - text-decoration: none; - - &:hover, - &:focus, - &:active { - text-decoration: underline; - } - } - - dl:last-child { - border-bottom: 0; - } -} diff --git a/app/javascript/flavours/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/stream_entries.scss index 45dcf70ed..6735049b9 100644 --- a/app/javascript/flavours/glitch/styles/stream_entries.scss +++ b/app/javascript/flavours/glitch/styles/stream_entries.scss @@ -192,6 +192,7 @@ .status__info .status__display-name { display: block; max-width: 100%; + padding: 6px 0; padding-right: 25px; margin: initial; diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index 645192ea4..307e509d5 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -352,6 +352,7 @@ border-radius: 50%; position: relative; margin-left: -10px; + background: darken($ui-base-color, 8%); border: 2px solid $ui-base-color; &:nth-child(1) { diff --git a/app/javascript/flavours/glitch/util/api.js b/app/javascript/flavours/glitch/util/api.js index 033d2d67b..c59a24518 100644 --- a/app/javascript/flavours/glitch/util/api.js +++ b/app/javascript/flavours/glitch/util/api.js @@ -13,10 +13,14 @@ export const getLinks = response => { }; let csrfHeader = {}; + function setCSRFHeader() { - const csrfToken = document.querySelector('meta[name=csrf-token]').content; - csrfHeader['X-CSRF-Token'] = csrfToken; + const csrfToken = document.querySelector('meta[name=csrf-token]'); + if (csrfToken) { + csrfHeader['X-CSRF-Token'] = csrfToken.content; + } } + ready(setCSRFHeader); export default getState => axios.create({ diff --git a/app/javascript/flavours/glitch/util/emoji/emoji_picker.js b/app/javascript/flavours/glitch/util/emoji/emoji_picker.js index 73fcaa8c8..044d38cb2 100644 --- a/app/javascript/flavours/glitch/util/emoji/emoji_picker.js +++ b/app/javascript/flavours/glitch/util/emoji/emoji_picker.js @@ -1,5 +1,5 @@ -import Picker from 'emoji-mart/dist-modern/components/picker/picker'; -import Emoji from 'emoji-mart/dist-modern/components/emoji/emoji'; +import Picker from 'emoji-mart/dist-es/components/picker/picker'; +import Emoji from 'emoji-mart/dist-es/components/emoji/emoji'; export { Picker, |