Federating conditional UI

This commit is contained in:
Alex Gleason 2021-08-23 14:14:47 -05:00
parent c18224f911
commit 6b19f39d51
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
8 changed files with 93 additions and 66 deletions

View File

@ -27,42 +27,37 @@ const allowedEmojiRGI = ImmutableList([
const year = new Date().getFullYear(); const year = new Date().getFullYear();
export const defaultConfig = ImmutableMap({ export const makeDefaultConfig = features => {
logo: '', return ImmutableMap({
banner: '', logo: '',
brandColor: '', // Empty banner: '',
customCss: ImmutableList(), brandColor: '', // Empty
promoPanel: ImmutableMap({ customCss: ImmutableList(),
items: ImmutableList(), promoPanel: ImmutableMap({
}), items: ImmutableList(),
extensions: ImmutableMap(), }),
defaultSettings: ImmutableMap(), extensions: ImmutableMap(),
copyright: `${year}. Copying is an act of love. Please copy and share.`, defaultSettings: ImmutableMap(),
navlinks: ImmutableMap({ copyright: `${year}. Copying is an act of love. Please copy and share.`,
homeFooter: ImmutableList(), navlinks: ImmutableMap({
}), homeFooter: ImmutableList(),
allowedEmoji: allowedEmoji, }),
verifiedCanEditName: false, allowedEmoji: features.emojiReactsRGI ? allowedEmojiRGI : allowedEmoji,
displayFqn: true, verifiedCanEditName: false,
cryptoAddresses: ImmutableList(), displayFqn: Boolean(features.federating),
cryptoDonatePanel: ImmutableMap({ cryptoAddresses: ImmutableList(),
limit: 1, cryptoDonatePanel: ImmutableMap({
}), limit: 1,
aboutPages: ImmutableMap(), }),
}); aboutPages: ImmutableMap(),
});
};
export const getSoapboxConfig = createSelector([ export const getSoapboxConfig = createSelector([
state => state.get('soapbox'), state => state.get('soapbox'),
state => getFeatures(state.get('instance')).emojiReactsRGI, state => getFeatures(state.get('instance')),
], (soapbox, emojiReactsRGI) => { ], (soapbox, features) => {
// https://git.pleroma.social/pleroma/pleroma/-/issues/2355 return makeDefaultConfig(features).merge(soapbox);
if (emojiReactsRGI) {
return defaultConfig
.set('allowedEmoji', allowedEmojiRGI)
.merge(soapbox);
} else {
return defaultConfig.merge(soapbox);
}
}); });
export function fetchSoapboxConfig() { export function fetchSoapboxConfig() {

View File

@ -10,11 +10,13 @@ import { Link } from 'react-router-dom';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import { fetchLists } from 'soapbox/actions/lists'; import { fetchLists } from 'soapbox/actions/lists';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({ const messages = defineMessages({
show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
homeTitle: { id: 'home_column_header.home', defaultMessage: 'Home' }, homeTitle: { id: 'home_column_header.home', defaultMessage: 'Home' },
allTitle: { id: 'home_column_header.all', defaultMessage: 'All' },
fediverseTitle: { id: 'home_column_header.fediverse', defaultMessage: 'Fediverse' }, fediverseTitle: { id: 'home_column_header.fediverse', defaultMessage: 'Fediverse' },
listTitle: { id: 'home_column.lists', defaultMessage: 'Lists' }, listTitle: { id: 'home_column.lists', defaultMessage: 'Lists' },
}); });
@ -28,9 +30,13 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => {
}); });
const mapStateToProps = state => { const mapStateToProps = state => {
const instance = state.get('instance');
const features = getFeatures(instance);
return { return {
lists: getOrderedLists(state), lists: getOrderedLists(state),
siteTitle: state.getIn(['instance', 'title']), siteTitle: state.getIn(['instance', 'title']),
federating: features.federating,
}; };
}; };
@ -49,6 +55,7 @@ class ColumnHeader extends React.PureComponent {
activeSubItem: PropTypes.string, activeSubItem: PropTypes.string,
lists: ImmutablePropTypes.list, lists: ImmutablePropTypes.list,
siteTitle: PropTypes.string, siteTitle: PropTypes.string,
federating: PropTypes.bool,
}; };
state = { state = {
@ -77,7 +84,7 @@ class ColumnHeader extends React.PureComponent {
} }
render() { render() {
const { active, children, intl: { formatMessage }, activeItem, activeSubItem, lists, siteTitle } = this.props; const { active, children, intl: { formatMessage }, activeItem, activeSubItem, lists, siteTitle, federating } = this.props;
const { collapsed, animating, expandedFor } = this.state; const { collapsed, animating, expandedFor } = this.state;
const wrapperClassName = classNames('column-header__wrapper', { const wrapperClassName = classNames('column-header__wrapper', {
@ -143,14 +150,14 @@ class ColumnHeader extends React.PureComponent {
</Link> </Link>
<Link to='/timeline/local' className={classNames('btn grouped', { 'active': 'local' === activeItem })}> <Link to='/timeline/local' className={classNames('btn grouped', { 'active': 'local' === activeItem })}>
<Icon id='users' fixedWidth className='column-header__icon' /> <Icon id={federating ? 'users' : 'globe-w'} fixedWidth className='column-header__icon' />
{siteTitle} {federating ? siteTitle : formatMessage(messages.allTitle)}
</Link> </Link>
<Link to='/timeline/fediverse' className={classNames('btn grouped', { 'active': 'fediverse' === activeItem })}> {federating && <Link to='/timeline/fediverse' className={classNames('btn grouped', { 'active': 'fediverse' === activeItem })}>
<Icon id='fediverse' fixedWidth className='column-header__icon' /> <Icon id='fediverse' fixedWidth className='column-header__icon' />
{formatMessage(messages.fediverseTitle)} {formatMessage(messages.fediverseTitle)}
</Link> </Link>}
<div className='column-header__buttons'> <div className='column-header__buttons'>
{collapseButton} {collapseButton}

View File

@ -14,6 +14,8 @@ import {
isModerator, isModerator,
isVerified, isVerified,
isLocal, isLocal,
isRemote,
getDomain,
} from 'soapbox/utils/accounts'; } from 'soapbox/utils/accounts';
import { parseVersion } from 'soapbox/utils/features'; import { parseVersion } from 'soapbox/utils/features';
import classNames from 'classnames'; import classNames from 'classnames';
@ -195,8 +197,8 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport }); menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
} }
if (account.get('acct') !== account.get('username')) { if (isRemote(account)) {
const domain = account.get('acct').split('@')[1]; const domain = getDomain(account);
menu.push(null); menu.push(null);

View File

@ -18,7 +18,8 @@ import {
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import { updateConfig } from 'soapbox/actions/admin'; import { updateConfig } from 'soapbox/actions/admin';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
import { defaultConfig } from 'soapbox/actions/soapbox'; import { makeDefaultConfig } from 'soapbox/actions/soapbox';
import { getFeatures } from 'soapbox/utils/features';
import { uploadMedia } from 'soapbox/actions/media'; import { uploadMedia } from 'soapbox/actions/media';
import { SketchPicker } from 'react-color'; import { SketchPicker } from 'react-color';
import Overlay from 'react-overlays/lib/Overlay'; import Overlay from 'react-overlays/lib/Overlay';
@ -60,9 +61,14 @@ const templates = {
cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }), cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }),
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => {
soapbox: state.get('soapbox'), const instance = state.get('instance');
});
return {
soapbox: state.get('soapbox'),
features: getFeatures(instance),
};
};
export default @connect(mapStateToProps) export default @connect(mapStateToProps)
@injectIntl @injectIntl
@ -70,6 +76,7 @@ class SoapboxConfig extends ImmutablePureComponent {
static propTypes = { static propTypes = {
soapbox: ImmutablePropTypes.map.isRequired, soapbox: ImmutablePropTypes.map.isRequired,
features: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -179,7 +186,9 @@ class SoapboxConfig extends ImmutablePureComponent {
} }
getSoapboxConfig = () => { getSoapboxConfig = () => {
return defaultConfig.mergeDeep(this.state.soapbox); const { features } = this.props;
const { soapbox } = this.state;
return makeDefaultConfig(features).mergeDeep(soapbox);
} }
toggleJSONEditor = (value) => this.setState({ jsonEditorExpanded: value }); toggleJSONEditor = (value) => this.setState({ jsonEditorExpanded: value });

View File

@ -8,11 +8,16 @@ import { openModal } from '../../../actions/modal';
import { logOut } from 'soapbox/actions/auth'; import { logOut } from 'soapbox/actions/auth';
import { isAdmin } from 'soapbox/utils/accounts'; import { isAdmin } from 'soapbox/utils/accounts';
import sourceCode from 'soapbox/utils/code'; import sourceCode from 'soapbox/utils/code';
import { getFeatures } from 'soapbox/utils/features';
const mapStateToProps = state => { const mapStateToProps = state => {
const me = state.get('me'); const me = state.get('me');
const instance = state.get('instance');
const features = getFeatures(instance);
return { return {
account: state.getIn(['accounts', me]), account: state.getIn(['accounts', me]),
federating: features.federating,
}; };
}; };
@ -26,19 +31,19 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}, },
}); });
const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => ( const LinkFooter = ({ onOpenHotkeys, account, federating, onClickLogOut }) => (
<div className='getting-started__footer'> <div className='getting-started__footer'>
<ul> <ul>
{account && <> {account && <>
<li><Link to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></Link></li> <li><Link to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></Link></li>
<li><Link to='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></Link></li> <li><Link to='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></Link></li>
<li><Link to='/filters'><FormattedMessage id='navigation_bar.filters' defaultMessage='Filters' /></Link></li> <li><Link to='/filters'><FormattedMessage id='navigation_bar.filters' defaultMessage='Filters' /></Link></li>
<li><Link to='/domain_blocks'><FormattedMessage id='navigation_bar.domain_blocks' defaultMessage='Domain blocks' /></Link></li> {federating && <li><Link to='/domain_blocks'><FormattedMessage id='navigation_bar.domain_blocks' defaultMessage='Domain blocks' /></Link></li>}
<li><Link to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></Link></li> <li><Link to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></Link></li>
{isAdmin(account) && <li><a href='/pleroma/admin'><FormattedMessage id='navigation_bar.admin_settings' defaultMessage='AdminFE' /></a></li>} {isAdmin(account) && <li><a href='/pleroma/admin'><FormattedMessage id='navigation_bar.admin_settings' defaultMessage='AdminFE' /></a></li>}
{isAdmin(account) && <li><Link to='/soapbox/config'><FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' /></Link></li>} {isAdmin(account) && <li><Link to='/soapbox/config'><FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' /></Link></li>}
<li><Link to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></Link></li> <li><Link to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></Link></li>
<li><Link to='/settings/aliases'><FormattedMessage id='navigation_bar.account_aliases' defaultMessage='Account aliases' /></Link></li> {federating && <li><Link to='/settings/aliases'><FormattedMessage id='navigation_bar.account_aliases' defaultMessage='Account aliases' /></Link></li>}
<li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li> <li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li>
</>} </>}
<li><Link to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></Link></li> <li><Link to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></Link></li>
@ -61,6 +66,7 @@ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => (
LinkFooter.propTypes = { LinkFooter.propTypes = {
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
federating: PropTypes.bool,
onOpenHotkeys: PropTypes.func.isRequired, onOpenHotkeys: PropTypes.func.isRequired,
onClickLogOut: PropTypes.func.isRequired, onClickLogOut: PropTypes.func.isRequired,
}; };

View File

@ -13,6 +13,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import Button from '../../../components/button'; import Button from '../../../components/button';
import Toggle from 'react-toggle'; import Toggle from 'react-toggle';
import IconButton from '../../../components/icon_button'; import IconButton from '../../../components/icon_button';
import { isRemote, getDomain } from 'soapbox/utils/accounts';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({ const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' }, close: { id: 'lightbox.close', defaultMessage: 'Close' },
@ -25,14 +27,18 @@ const makeMapStateToProps = () => {
const mapStateToProps = state => { const mapStateToProps = state => {
const accountId = state.getIn(['reports', 'new', 'account_id']); const accountId = state.getIn(['reports', 'new', 'account_id']);
const account = getAccount(state, accountId);
const instance = state.get('instance');
const features = getFeatures(instance);
return { return {
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
account: getAccount(state, accountId), account,
comment: state.getIn(['reports', 'new', 'comment']), comment: state.getIn(['reports', 'new', 'comment']),
forward: state.getIn(['reports', 'new', 'forward']), forward: state.getIn(['reports', 'new', 'forward']),
block: state.getIn(['reports', 'new', 'block']), block: state.getIn(['reports', 'new', 'block']),
statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])),
canForward: isRemote(account) && features.federating,
}; };
}; };
@ -50,6 +56,7 @@ class ReportModal extends ImmutablePureComponent {
comment: PropTypes.string.isRequired, comment: PropTypes.string.isRequired,
forward: PropTypes.bool, forward: PropTypes.bool,
block: PropTypes.bool, block: PropTypes.bool,
canForward: PropTypes.bool,
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
@ -91,14 +98,12 @@ class ReportModal extends ImmutablePureComponent {
} }
render() { render() {
const { account, comment, intl, statusIds, isSubmitting, forward, block, onClose } = this.props; const { account, comment, intl, statusIds, isSubmitting, forward, block, canForward, onClose } = this.props;
if (!account) { if (!account) {
return null; return null;
} }
const domain = account.get('acct').split('@')[1];
return ( return (
<div className='modal-root__modal report-modal'> <div className='modal-root__modal report-modal'>
<div className='report-modal__target'> <div className='report-modal__target'>
@ -120,13 +125,13 @@ class ReportModal extends ImmutablePureComponent {
autoFocus autoFocus
/> />
{domain && ( {canForward && (
<div> <div>
<p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send a copy of the report there as well?' /></p> <p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send a copy of the report there as well?' /></p>
<div className='setting-toggle'> <div className='setting-toggle'>
<Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} /> <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
<label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} /></label> <label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: getDomain(account) }} /></label>
</div> </div>
</div> </div>
)} )}

View File

@ -1,24 +1,23 @@
import { Map as ImmutableMap } from 'immutable'; import { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
const guessDomain = account => { const getDomainFromURL = account => {
try { try {
const re = /https?:\/\/(.*?)\//i; const url = account.get('url');
return re.exec(account.get('url'))[1]; return new URL(url).host;
} catch(e) { } catch {
return null; return '';
} }
}; };
export const getDomain = account => { export const getDomain = account => {
let domain = account.get('acct').split('@')[1]; const domain = account.get('acct').split('@')[1];
if (!domain) domain = guessDomain(account); return domain ? domain : getDomainFromURL(account);
return domain;
}; };
export const guessFqn = account => { export const guessFqn = account => {
const [user, domain] = account.get('acct').split('@'); const [user, domain] = account.get('acct').split('@');
if (!domain) return [user, guessDomain(account)].join('@'); if (!domain) return [user, getDomainFromURL(account)].join('@');
return account.get('acct'); return account.get('acct');
}; };
@ -54,6 +53,8 @@ export const isLocal = account => {
return domain === undefined ? true : false; return domain === undefined ? true : false;
}; };
export const isRemote = account => !isLocal(account);
export const isVerified = account => ( export const isVerified = account => (
account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified') account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified')
); );

View File

@ -1,12 +1,13 @@
// Detect backend features to conditionally render elements // Detect backend features to conditionally render elements
import gte from 'semver/functions/gte'; import gte from 'semver/functions/gte';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
export const getFeatures = createSelector([ export const getFeatures = createSelector([
instance => parseVersion(instance.get('version')), instance => parseVersion(instance.get('version')),
instance => instance.getIn(['pleroma', 'metadata', 'features'], ImmutableList()), instance => instance.getIn(['pleroma', 'metadata', 'features'], ImmutableList()),
], (v, f) => { instance => instance.getIn(['pleroma', 'metadata', 'federation'], ImmutableMap()),
], (v, features, federation) => {
return { return {
suggestions: v.software === 'Mastodon' && gte(v.compatVersion, '2.4.3'), suggestions: v.software === 'Mastodon' && gte(v.compatVersion, '2.4.3'),
trends: v.software === 'Mastodon' && gte(v.compatVersion, '3.0.0'), trends: v.software === 'Mastodon' && gte(v.compatVersion, '3.0.0'),
@ -15,9 +16,10 @@ export const getFeatures = createSelector([
attachmentLimit: v.software === 'Pleroma' ? Infinity : 4, attachmentLimit: v.software === 'Pleroma' ? Infinity : 4,
focalPoint: v.software === 'Mastodon' && gte(v.compatVersion, '2.3.0'), focalPoint: v.software === 'Mastodon' && gte(v.compatVersion, '2.3.0'),
importMutes: v.software === 'Pleroma' && gte(v.version, '2.2.0'), importMutes: v.software === 'Pleroma' && gte(v.version, '2.2.0'),
emailList: f.includes('email_list'), emailList: features.includes('email_list'),
chats: v.software === 'Pleroma' && gte(v.version, '2.1.0'), chats: v.software === 'Pleroma' && gte(v.version, '2.1.0'),
scopes: v.software === 'Pleroma' ? 'read write follow push admin' : 'read write follow push', scopes: v.software === 'Pleroma' ? 'read write follow push admin' : 'read write follow push',
federating: federation.get('enabled', true), // Assume true unless explicitly false
}; };
}); });