diff --git a/app/soapbox/actions/soapbox.js b/app/soapbox/actions/soapbox.js index 25e9dcecf..88109b7a0 100644 --- a/app/soapbox/actions/soapbox.js +++ b/app/soapbox/actions/soapbox.js @@ -27,42 +27,37 @@ const allowedEmojiRGI = ImmutableList([ const year = new Date().getFullYear(); -export const defaultConfig = ImmutableMap({ - logo: '', - banner: '', - brandColor: '', // Empty - customCss: ImmutableList(), - promoPanel: ImmutableMap({ - items: ImmutableList(), - }), - extensions: ImmutableMap(), - defaultSettings: ImmutableMap(), - copyright: `♥${year}. Copying is an act of love. Please copy and share.`, - navlinks: ImmutableMap({ - homeFooter: ImmutableList(), - }), - allowedEmoji: allowedEmoji, - verifiedCanEditName: false, - displayFqn: true, - cryptoAddresses: ImmutableList(), - cryptoDonatePanel: ImmutableMap({ - limit: 1, - }), - aboutPages: ImmutableMap(), -}); +export const makeDefaultConfig = features => { + return ImmutableMap({ + logo: '', + banner: '', + brandColor: '', // Empty + customCss: ImmutableList(), + promoPanel: ImmutableMap({ + items: ImmutableList(), + }), + extensions: ImmutableMap(), + defaultSettings: ImmutableMap(), + copyright: `♥${year}. Copying is an act of love. Please copy and share.`, + navlinks: ImmutableMap({ + homeFooter: ImmutableList(), + }), + allowedEmoji: features.emojiReactsRGI ? allowedEmojiRGI : allowedEmoji, + verifiedCanEditName: false, + displayFqn: Boolean(features.federating), + cryptoAddresses: ImmutableList(), + cryptoDonatePanel: ImmutableMap({ + limit: 1, + }), + aboutPages: ImmutableMap(), + }); +}; export const getSoapboxConfig = createSelector([ state => state.get('soapbox'), - state => getFeatures(state.get('instance')).emojiReactsRGI, -], (soapbox, emojiReactsRGI) => { - // https://git.pleroma.social/pleroma/pleroma/-/issues/2355 - if (emojiReactsRGI) { - return defaultConfig - .set('allowedEmoji', allowedEmojiRGI) - .merge(soapbox); - } else { - return defaultConfig.merge(soapbox); - } + state => getFeatures(state.get('instance')), +], (soapbox, features) => { + return makeDefaultConfig(features).merge(soapbox); }); export function fetchSoapboxConfig() { diff --git a/app/soapbox/components/home_column_header.js b/app/soapbox/components/home_column_header.js index 21bc3d331..9ed7fea2a 100644 --- a/app/soapbox/components/home_column_header.js +++ b/app/soapbox/components/home_column_header.js @@ -10,11 +10,13 @@ import { Link } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import { fetchLists } from 'soapbox/actions/lists'; import { createSelector } from 'reselect'; +import { getFeatures } from 'soapbox/utils/features'; const messages = defineMessages({ show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' }, hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' }, homeTitle: { id: 'home_column_header.home', defaultMessage: 'Home' }, + allTitle: { id: 'home_column_header.all', defaultMessage: 'All' }, fediverseTitle: { id: 'home_column_header.fediverse', defaultMessage: 'Fediverse' }, listTitle: { id: 'home_column.lists', defaultMessage: 'Lists' }, }); @@ -28,9 +30,13 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => { }); const mapStateToProps = state => { + const instance = state.get('instance'); + const features = getFeatures(instance); + return { lists: getOrderedLists(state), siteTitle: state.getIn(['instance', 'title']), + federating: features.federating, }; }; @@ -49,6 +55,7 @@ class ColumnHeader extends React.PureComponent { activeSubItem: PropTypes.string, lists: ImmutablePropTypes.list, siteTitle: PropTypes.string, + federating: PropTypes.bool, }; state = { @@ -77,7 +84,7 @@ class ColumnHeader extends React.PureComponent { } 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 wrapperClassName = classNames('column-header__wrapper', { @@ -143,14 +150,14 @@ class ColumnHeader extends React.PureComponent { - - {siteTitle} + + {federating ? siteTitle : formatMessage(messages.allTitle)} - + {federating && {formatMessage(messages.fediverseTitle)} - + }
{collapseButton} diff --git a/app/soapbox/features/account/components/header.js b/app/soapbox/features/account/components/header.js index 5ca1eb34f..3e4d152dd 100644 --- a/app/soapbox/features/account/components/header.js +++ b/app/soapbox/features/account/components/header.js @@ -14,6 +14,8 @@ import { isModerator, isVerified, isLocal, + isRemote, + getDomain, } from 'soapbox/utils/accounts'; import { parseVersion } from 'soapbox/utils/features'; 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 }); } - if (account.get('acct') !== account.get('username')) { - const domain = account.get('acct').split('@')[1]; + if (isRemote(account)) { + const domain = getDomain(account); menu.push(null); diff --git a/app/soapbox/features/soapbox_config/index.js b/app/soapbox/features/soapbox_config/index.js index 9754de627..b0199c449 100644 --- a/app/soapbox/features/soapbox_config/index.js +++ b/app/soapbox/features/soapbox_config/index.js @@ -18,7 +18,8 @@ import { import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { updateConfig } from 'soapbox/actions/admin'; 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 { SketchPicker } from 'react-color'; import Overlay from 'react-overlays/lib/Overlay'; @@ -60,9 +61,14 @@ const templates = { cryptoAddress: ImmutableMap({ ticker: '', address: '', note: '' }), }; -const mapStateToProps = state => ({ - soapbox: state.get('soapbox'), -}); +const mapStateToProps = state => { + const instance = state.get('instance'); + + return { + soapbox: state.get('soapbox'), + features: getFeatures(instance), + }; +}; export default @connect(mapStateToProps) @injectIntl @@ -70,6 +76,7 @@ class SoapboxConfig extends ImmutablePureComponent { static propTypes = { soapbox: ImmutablePropTypes.map.isRequired, + features: PropTypes.object.isRequired, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -179,7 +186,9 @@ class SoapboxConfig extends ImmutablePureComponent { } 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 }); diff --git a/app/soapbox/features/ui/components/link_footer.js b/app/soapbox/features/ui/components/link_footer.js index 46b84a27c..29b22f8e0 100644 --- a/app/soapbox/features/ui/components/link_footer.js +++ b/app/soapbox/features/ui/components/link_footer.js @@ -8,11 +8,16 @@ import { openModal } from '../../../actions/modal'; import { logOut } from 'soapbox/actions/auth'; import { isAdmin } from 'soapbox/utils/accounts'; import sourceCode from 'soapbox/utils/code'; +import { getFeatures } from 'soapbox/utils/features'; const mapStateToProps = state => { const me = state.get('me'); + const instance = state.get('instance'); + const features = getFeatures(instance); + return { 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 }) => (
    {account && <>
  • -
  • + {federating &&
  • }
  • {isAdmin(account) &&
  • } {isAdmin(account) &&
  • }
  • -
  • + {federating &&
  • }
  • }
  • @@ -61,6 +66,7 @@ const LinkFooter = ({ onOpenHotkeys, account, onClickLogOut }) => ( LinkFooter.propTypes = { account: ImmutablePropTypes.map, + federating: PropTypes.bool, onOpenHotkeys: PropTypes.func.isRequired, onClickLogOut: PropTypes.func.isRequired, }; diff --git a/app/soapbox/features/ui/components/report_modal.js b/app/soapbox/features/ui/components/report_modal.js index dca4e1118..fd34da406 100644 --- a/app/soapbox/features/ui/components/report_modal.js +++ b/app/soapbox/features/ui/components/report_modal.js @@ -13,6 +13,8 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import Button from '../../../components/button'; import Toggle from 'react-toggle'; import IconButton from '../../../components/icon_button'; +import { isRemote, getDomain } from 'soapbox/utils/accounts'; +import { getFeatures } from 'soapbox/utils/features'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -25,14 +27,18 @@ const makeMapStateToProps = () => { const mapStateToProps = state => { const accountId = state.getIn(['reports', 'new', 'account_id']); + const account = getAccount(state, accountId); + const instance = state.get('instance'); + const features = getFeatures(instance); return { isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), - account: getAccount(state, accountId), + account, comment: state.getIn(['reports', 'new', 'comment']), forward: state.getIn(['reports', 'new', 'forward']), block: state.getIn(['reports', 'new', 'block']), 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, forward: PropTypes.bool, block: PropTypes.bool, + canForward: PropTypes.bool, dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -91,14 +98,12 @@ class ReportModal extends ImmutablePureComponent { } 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) { return null; } - const domain = account.get('acct').split('@')[1]; - return (
    @@ -120,13 +125,13 @@ class ReportModal extends ImmutablePureComponent { autoFocus /> - {domain && ( + {canForward && (

    - +
    )} diff --git a/app/soapbox/utils/accounts.js b/app/soapbox/utils/accounts.js index 77e3f3997..461fca42e 100644 --- a/app/soapbox/utils/accounts.js +++ b/app/soapbox/utils/accounts.js @@ -1,24 +1,23 @@ import { Map as ImmutableMap } from 'immutable'; import { List as ImmutableList } from 'immutable'; -const guessDomain = account => { +const getDomainFromURL = account => { try { - const re = /https?:\/\/(.*?)\//i; - return re.exec(account.get('url'))[1]; - } catch(e) { - return null; + const url = account.get('url'); + return new URL(url).host; + } catch { + return ''; } }; export const getDomain = account => { - let domain = account.get('acct').split('@')[1]; - if (!domain) domain = guessDomain(account); - return domain; + const domain = account.get('acct').split('@')[1]; + return domain ? domain : getDomainFromURL(account); }; export const guessFqn = account => { 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'); }; @@ -54,6 +53,8 @@ export const isLocal = account => { return domain === undefined ? true : false; }; +export const isRemote = account => !isLocal(account); + export const isVerified = account => ( account.getIn(['pleroma', 'tags'], ImmutableList()).includes('verified') ); diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js index 2daa50546..32b37f85c 100644 --- a/app/soapbox/utils/features.js +++ b/app/soapbox/utils/features.js @@ -1,12 +1,13 @@ // Detect backend features to conditionally render elements 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'; export const getFeatures = createSelector([ instance => parseVersion(instance.get('version')), instance => instance.getIn(['pleroma', 'metadata', 'features'], ImmutableList()), -], (v, f) => { + instance => instance.getIn(['pleroma', 'metadata', 'federation'], ImmutableMap()), +], (v, features, federation) => { return { suggestions: v.software === 'Mastodon' && gte(v.compatVersion, '2.4.3'), trends: v.software === 'Mastodon' && gte(v.compatVersion, '3.0.0'), @@ -15,9 +16,10 @@ export const getFeatures = createSelector([ attachmentLimit: v.software === 'Pleroma' ? Infinity : 4, focalPoint: v.software === 'Mastodon' && gte(v.compatVersion, '2.3.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'), scopes: v.software === 'Pleroma' ? 'read write follow push admin' : 'read write follow push', + federating: federation.get('enabled', true), // Assume true unless explicitly false }; });