Merge branch 'mastodong' into 'develop'

Mastodon: conditionally display Markdown, emojiReact column settings, sidebar...

See merge request soapbox-pub/soapbox-fe!690
reply-to-ui
Alex Gleason 2021-08-23 23:13:20 +00:00
commit 3dacb5448a
19 zmienionych plików z 191 dodań i 69 usunięć

Wyświetl plik

@ -55,8 +55,10 @@ export const changeAliasesSuggestions = value => ({
export const addToAliases = (intl, apId) => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const alsoKnownAs = getState().getIn(['meta', 'pleroma', 'also_known_as']);
const me = state.get('me');
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
dispatch(addToAliasesRequest(apId));
@ -92,8 +94,10 @@ export const addToAliasesFail = (apId, error) => ({
export const removeFromAliases = (intl, apId) => (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
const state = getState();
const alsoKnownAs = getState().getIn(['meta', 'pleroma', 'also_known_as']);
const me = state.get('me');
const alsoKnownAs = state.getIn(['accounts_meta', me, 'pleroma', 'also_known_as']);
dispatch(removeFromAliasesRequest(apId));

Wyświetl plik

@ -18,14 +18,17 @@ const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = (state, { accountId, added }) => {
const me = state.get('me');
const ownAccount = getAccount(state, me);
const account = getAccount(state, accountId);
const apId = account.getIn(['pleroma', 'ap_id']);
return {
account,
apId,
added: typeof added === 'undefined' ? state.getIn(['meta', 'pleroma', 'also_known_as']).includes(apId) : added,
me: state.get('me'),
added: typeof added === 'undefined' ? ownAccount.getIn(['pleroma', 'also_known_as']).includes(apId) : added,
me,
};
};

Wyświetl plik

@ -9,6 +9,7 @@ import Icon from 'soapbox/components/icon';
import Search from './components/search';
import Account from './components/account';
import { removeFromAliases } from '../../actions/aliases';
import { makeGetAccount } from 'soapbox/selectors';
const messages = defineMessages({
heading: { id: 'column.aliases', defaultMessage: 'Account aliases' },
@ -19,13 +20,24 @@ const messages = defineMessages({
delete: { id: 'column.aliases.delete', defaultMessage: 'Delete' },
});
const mapStateToProps = state => ({
aliases: state.getIn(['meta', 'pleroma', 'also_known_as']),
searchAccountIds: state.getIn(['aliases', 'suggestions', 'items']),
loaded: state.getIn(['aliases', 'suggestions', 'loaded']),
});
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
export default @connect(mapStateToProps)
const mapStateToProps = state => {
const me = state.get('me');
const account = getAccount(state, me);
return {
aliases: account.getIn(['pleroma', 'also_known_as']),
searchAccountIds: state.getIn(['aliases', 'suggestions', 'items']),
loaded: state.getIn(['aliases', 'suggestions', 'loaded']),
};
};
return mapStateToProps;
};
export default @connect(makeMapStateToProps)
@injectIntl
class Aliases extends ImmutablePureComponent {
@ -78,4 +90,4 @@ class Aliases extends ImmutablePureComponent {
);
}
}
}

Wyświetl plik

@ -9,6 +9,7 @@ export default class TextIconButton extends React.PureComponent {
active: PropTypes.bool,
onClick: PropTypes.func.isRequired,
ariaControls: PropTypes.string,
unavailable: PropTypes.bool,
};
handleClick = (e) => {
@ -17,7 +18,11 @@ export default class TextIconButton extends React.PureComponent {
}
render() {
const { label, title, active, ariaControls } = this.props;
const { label, title, active, ariaControls, unavailable } = this.props;
if (unavailable) {
return null;
}
return (
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}>

Wyświetl plik

@ -2,18 +2,25 @@ import { connect } from 'react-redux';
import TextIconButton from '../components/text_icon_button';
import { changeComposeContentType } from '../../../actions/compose';
import { injectIntl, defineMessages } from 'react-intl';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Post markdown enabled' },
unmarked: { id: 'compose_form.markdown.unmarked', defaultMessage: 'Post markdown disabled' },
});
const mapStateToProps = (state, { intl }) => ({
label: 'MD',
title: intl.formatMessage(state.getIn(['compose', 'content_type']) === 'text/markdown' ? messages.marked : messages.unmarked),
active: state.getIn(['compose', 'content_type']) === 'text/markdown',
ariaControls: 'markdown-input',
});
const mapStateToProps = (state, { intl }) => {
const instance = state.get('instance');
const features = getFeatures(instance);
return {
label: 'MD',
title: intl.formatMessage(state.getIn(['compose', 'content_type']) === 'text/markdown' ? messages.marked : messages.unmarked),
active: state.getIn(['compose', 'content_type']) === 'text/markdown',
ariaControls: 'markdown-input',
unavailable: !features.richText,
};
};
const mapDispatchToProps = dispatch => ({

Wyświetl plik

@ -26,6 +26,7 @@ import { unescape } from 'lodash';
import { isVerified } from 'soapbox/utils/accounts';
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
import { getFeatures } from 'soapbox/utils/features';
import { makeGetAccount } from 'soapbox/selectors';
const hidesNetwork = account => {
const pleroma = account.get('pleroma');
@ -41,19 +42,27 @@ const messages = defineMessages({
metaFieldContent: { id: 'edit_profile.fields.meta_fields.content_placeholder', defaultMessage: 'Content' },
verified: { id: 'edit_profile.fields.verified_display_name', defaultMessage: 'Verified users may not update their display name' },
success: { id: 'edit_profile.success', defaultMessage: 'Profile saved!' },
bioPlaceholder: { id: 'edit_profile.fields.bio_placeholder', defaultMessage: 'Tell us about yourself.' },
displayNamePlaceholder: { id: 'edit_profile.fields.display_name_placeholder', defaultMessage: 'Name' },
});
const mapStateToProps = state => {
const me = state.get('me');
const soapbox = getSoapboxConfig(state);
const meta = state.getIn(['meta', 'pleroma']);
const account = state.getIn(['accounts', me]).set('pleroma', meta);
return {
account,
maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4),
verifiedCanEditName: soapbox.get('verifiedCanEditName'),
supportsEmailList: getFeatures(state.get('instance')).emailList,
const makeMapStateToProps = () => {
const getAccount = makeGetAccount();
const mapStateToProps = state => {
const me = state.get('me');
const account = getAccount(state, me);
const soapbox = getSoapboxConfig(state);
return {
account,
maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4),
verifiedCanEditName: soapbox.get('verifiedCanEditName'),
supportsEmailList: getFeatures(state.get('instance')).emailList,
};
};
return mapStateToProps;
};
// Forces fields to be maxFields size, filling empty values
@ -70,7 +79,7 @@ const unescapeParams = (map, params) => (
), map)
);
export default @connect(mapStateToProps)
export default @connect(makeMapStateToProps)
@injectIntl
class EditProfile extends ImmutablePureComponent {
@ -88,18 +97,21 @@ class EditProfile extends ImmutablePureComponent {
constructor(props) {
super(props);
const { account } = this.props;
const { account, maxFields } = this.props;
const strangerNotifications = account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']);
const acceptsEmailList = account.getIn(['pleroma', 'accepts_email_list']);
const initialState = account.withMutations(map => {
map.merge(map.get('source'));
map.delete('source');
map.set('fields', normalizeFields(map.get('fields'), Math.min(props.maxFields, 4)));
map.set('fields', normalizeFields(map.get('fields'), Math.min(maxFields, 4)));
map.set('stranger_notifications', strangerNotifications);
map.set('accepts_email_list', acceptsEmailList);
map.set('hide_network', hidesNetwork(account));
unescapeParams(map, ['display_name', 'bio']);
});
this.state = initialState.toObject();
}
@ -108,7 +120,7 @@ class EditProfile extends ImmutablePureComponent {
return account.merge(ImmutableMap({
header: this.state.header,
avatar: this.state.avatar,
display_name: this.state.display_name,
display_name: this.state.display_name || account.get('username'),
}));
}
@ -225,6 +237,7 @@ class EditProfile extends ImmutablePureComponent {
<TextInput
className={canEditName ? '' : 'disabled'}
label={<FormattedMessage id='edit_profile.fields.display_name_label' defaultMessage='Display name' />}
placeholder={intl.formatMessage(messages.displayNamePlaceholder)}
name='display_name'
value={this.state.display_name}
onChange={this.handleTextChange}
@ -233,6 +246,7 @@ class EditProfile extends ImmutablePureComponent {
/>
<SimpleTextarea
label={<FormattedMessage id='edit_profile.fields.bio_label' defaultMessage='Bio' />}
placeholder={intl.formatMessage(messages.bioPlaceholder)}
name='note'
autoComplete='off'
value={this.state.note}

Wyświetl plik

@ -13,6 +13,7 @@ export default class ColumnSettings extends React.PureComponent {
pushSettings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
supportsEmojiReacts: PropTypes.bool,
};
onPushChange = (path, checked) => {
@ -28,7 +29,7 @@ export default class ColumnSettings extends React.PureComponent {
}
render() {
const { settings, pushSettings, onChange, onClear } = this.props;
const { settings, pushSettings, onChange, onClear, supportsEmojiReacts } = this.props;
const filterShowStr = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
@ -96,7 +97,7 @@ export default class ColumnSettings extends React.PureComponent {
</div>
</div>
<div role='group' aria-labelledby='notifications-emoji-react'>
{supportsEmojiReacts && <div role='group' aria-labelledby='notifications-emoji-react'>
<span id='notifications-favourite' className='column-settings__section'><FormattedMessage id='notifications.column_settings.emoji_react' defaultMessage='Emoji reacts:' /></span>
<div className='column-settings__row'>
@ -105,7 +106,7 @@ export default class ColumnSettings extends React.PureComponent {
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'pleroma:emoji_reaction']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'pleroma:emoji_reaction']} onChange={onChange} label={soundStr} />
</div>
</div>
</div>}
<div role='group' aria-labelledby='notifications-mention'>
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>

Wyświetl plik

@ -20,6 +20,7 @@ class FilterBar extends React.PureComponent {
selectFilter: PropTypes.func.isRequired,
selectedFilter: PropTypes.string.isRequired,
advancedMode: PropTypes.bool.isRequired,
supportsEmojiReacts: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
@ -28,7 +29,7 @@ class FilterBar extends React.PureComponent {
}
render() {
const { selectedFilter, advancedMode, intl } = this.props;
const { selectedFilter, advancedMode, supportsEmojiReacts, intl } = this.props;
const renderedElement = !advancedMode ? (
<div className='notification__filter-bar'>
<button
@ -75,13 +76,13 @@ class FilterBar extends React.PureComponent {
>
<Icon id='thumbs-up' fixedWidth />
</button>
<button
{supportsEmojiReacts && <button
className={selectedFilter === 'pleroma:emoji_reaction' ? 'active' : ''}
onClick={this.onClick('pleroma:emoji_reaction')}
title={intl.formatMessage(tooltips.emoji_reacts)}
>
<Icon id='smile-o' fixedWidth />
</button>
</button>}
<button
className={selectedFilter === 'reblog' ? 'active' : ''}
onClick={this.onClick('reblog')}

Wyświetl plik

@ -6,16 +6,23 @@ import { setFilter } from '../../../actions/notifications';
import { clearNotifications } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
import { openModal } from '../../../actions/modal';
import { getFeatures } from 'soapbox/utils/features';
const messages = defineMessages({
clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' },
clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' },
});
const mapStateToProps = state => ({
settings: getSettings(state).get('notifications'),
pushSettings: state.get('push_notifications'),
});
const mapStateToProps = state => {
const instance = state.get('instance');
const features = getFeatures(instance);
return {
settings: getSettings(state).get('notifications'),
pushSettings: state.get('push_notifications'),
supportsEmojiReacts: features.emojiReacts,
};
};
const mapDispatchToProps = (dispatch, { intl }) => ({

Wyświetl plik

@ -2,12 +2,17 @@ import { connect } from 'react-redux';
import FilterBar from '../components/filter_bar';
import { setFilter } from '../../../actions/notifications';
import { getSettings } from 'soapbox/actions/settings';
import { getFeatures } from 'soapbox/utils/features';
const makeMapStateToProps = state => {
const settings = getSettings(state);
const instance = state.get('instance');
const features = getFeatures(instance);
return {
selectedFilter: settings.getIn(['notifications', 'quickFilter', 'active']),
advancedMode: settings.getIn(['notifications', 'quickFilter', 'advanced']),
supportsEmojiReacts: features.emojiReacts,
};
};

Wyświetl plik

@ -6,6 +6,8 @@ import IconWithCounter from 'soapbox/components/icon_with_counter';
import { NavLink } from 'react-router-dom';
import { injectIntl, defineMessages } from 'react-intl';
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
import { getFeatures } from 'soapbox/utils/features';
import { getBaseURL } from 'soapbox/utils/accounts';
const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit Profile' },
@ -18,9 +20,16 @@ const messages = defineMessages({
const mapStateToProps = state => {
const me = state.get('me');
const account = state.getIn(['accounts', me]);
const instance = state.get('instance');
const features = getFeatures(instance);
return {
isLocked: state.getIn(['accounts', me, 'locked']),
followRequestsCount: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableOrderedSet()).count(),
baseURL: getBaseURL(account),
features,
};
};
@ -32,10 +41,12 @@ class FeaturesPanel extends React.PureComponent {
intl: PropTypes.object.isRequired,
isLocked: PropTypes.bool,
followRequestsCount: PropTypes.number,
baseURL: PropTypes.string,
features: PropTypes.object.isRequired,
};
render() {
const { intl, isLocked, followRequestsCount } = this.props;
const { intl, isLocked, followRequestsCount, baseURL, features } = this.props;
return (
<div className='wtf-panel promo-panel'>
@ -61,15 +72,29 @@ class FeaturesPanel extends React.PureComponent {
{intl.formatMessage(messages.lists)}
</NavLink>
<NavLink className='promo-panel-item' to='/auth/edit'>
<Icon id='lock' className='promo-panel-item__icon' fixedWidth />
{intl.formatMessage(messages.security)}
</NavLink>
{features.securityAPI ? (
<NavLink className='promo-panel-item' to='/auth/edit'>
<Icon id='lock' className='promo-panel-item__icon' fixedWidth />
{intl.formatMessage(messages.security)}
</NavLink>
) : (
<a className='promo-panel-item' href={`${baseURL}/auth/edit`}>
<Icon id='lock' className='promo-panel-item__icon' fixedWidth />
{intl.formatMessage(messages.security)}
</a>
)}
<NavLink className='promo-panel-item' to='/settings/preferences'>
<Icon id='cog' className='promo-panel-item__icon' fixedWidth />
{intl.formatMessage(messages.preferences)}
</NavLink>
{features.settingsStore ? (
<NavLink className='promo-panel-item' to='/settings/preferences'>
<Icon id='cog' className='promo-panel-item__icon' fixedWidth />
{intl.formatMessage(messages.preferences)}
</NavLink>
) : (
<a className='promo-panel-item' href={`${baseURL}/settings/preferences`}>
<Icon id='cog' className='promo-panel-item__icon' fixedWidth />
{intl.formatMessage(messages.preferences)}
</a>
)}
</div>
</div>

Wyświetl plik

@ -18,6 +18,7 @@ const mapStateToProps = state => {
return {
account: state.getIn(['accounts', me]),
federating: features.federating,
showAliases: features.accountAliasesAPI,
};
};
@ -31,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
},
});
const LinkFooter = ({ onOpenHotkeys, account, federating, onClickLogOut }) => (
const LinkFooter = ({ onOpenHotkeys, account, federating, showAliases, onClickLogOut }) => (
<div className='getting-started__footer'>
<ul>
{account && <>
@ -43,7 +44,7 @@ const LinkFooter = ({ onOpenHotkeys, account, federating, onClickLogOut }) => (
{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>}
<li><Link to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></Link></li>
{federating && <li><Link to='/settings/aliases'><FormattedMessage id='navigation_bar.account_aliases' defaultMessage='Account aliases' /></Link></li>}
{(federating && showAliases) && <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><Link to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></Link></li>
@ -67,6 +68,7 @@ const LinkFooter = ({ onOpenHotkeys, account, federating, onClickLogOut }) => (
LinkFooter.propTypes = {
account: ImmutablePropTypes.map,
federating: PropTypes.bool,
showAliases: PropTypes.bool,
onOpenHotkeys: PropTypes.func.isRequired,
onClickLogOut: PropTypes.func.isRequired,
};

Wyświetl plik

@ -46,6 +46,7 @@ const normalizeAccount = (state, account) => {
'followers_count',
'following_count',
'statuses_count',
'source',
]);
return state.set(account.id, normalized);

Wyświetl plik

@ -0,0 +1,28 @@
/**
* Accounts Meta: private user data only the owner should see.
* @module soapbox/reducers/accounts_meta
*/
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
import { Map as ImmutableMap, fromJS } from 'immutable';
const initialState = ImmutableMap();
const importAccount = (state, account) => {
const accountId = account.get('id');
return state.set(accountId, ImmutableMap({
pleroma: account.get('pleroma', ImmutableMap()).delete('settings_store'),
source: account.get('source', ImmutableMap()),
}));
};
export default function accounts_meta(state = initialState, action) {
switch(action.type) {
case ME_FETCH_SUCCESS:
case ME_PATCH_SUCCESS:
return importAccount(state, fromJS(action.me));
default:
return state;
}
}

Wyświetl plik

@ -54,6 +54,7 @@ import admin_log from './admin_log';
import security from './security';
import scheduled_statuses from './scheduled_statuses';
import aliases from './aliases';
import accounts_meta from './accounts_meta';
const appReducer = combineReducers({
dropdown_menu,
@ -109,6 +110,7 @@ const appReducer = combineReducers({
security,
scheduled_statuses,
aliases,
accounts_meta,
});
// Clear the state (mostly) when the user logs out

Wyświetl plik

@ -1,25 +1,12 @@
'use strict';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS } from 'soapbox/actions/me';
import { INSTANCE_FETCH_FAIL } from 'soapbox/actions/instance';
import { Map as ImmutableMap, fromJS } from 'immutable';
import { Map as ImmutableMap } from 'immutable';
const initialState = ImmutableMap();
const importAccount = (state, account) => {
return state.withMutations(state => {
if (account.has('pleroma')) {
const pleroPrefs = account.get('pleroma').delete('settings_store');
state.mergeIn(['pleroma'], pleroPrefs);
}
});
};
export default function meta(state = initialState, action) {
switch(action.type) {
case ME_FETCH_SUCCESS:
case ME_PATCH_SUCCESS:
return importAccount(state, fromJS(action.me));
case INSTANCE_FETCH_FAIL:
return state.set('instance_fetch_failed', true);
default:

Wyświetl plik

@ -14,6 +14,7 @@ const getAccountBase = (state, id) => state.getIn(['accounts', id], null
const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null);
const getAccountRelationship = (state, id) => state.getIn(['relationships', id], null);
const getAccountMoved = (state, id) => state.getIn(['accounts', state.getIn(['accounts', id, 'moved'])]);
const getAccountMeta = (state, id) => state.getIn(['accounts_meta', id], ImmutableMap());
const getAccountAdminData = (state, id) => state.getIn(['admin', 'users', id]);
const getAccountPatron = (state, id) => {
const url = state.getIn(['accounts', id, 'url']);
@ -26,14 +27,18 @@ export const makeGetAccount = () => {
getAccountCounters,
getAccountRelationship,
getAccountMoved,
getAccountMeta,
getAccountAdminData,
getAccountPatron,
], (base, counters, relationship, moved, admin, patron) => {
], (base, counters, relationship, moved, meta, admin, patron) => {
if (base === null) {
return null;
}
return base.merge(counters).withMutations(map => {
return base.withMutations(map => {
map.merge(counters);
map.merge(meta);
map.set('pleroma', meta.get('pleroma', ImmutableMap()).merge(base.get('pleroma', ImmutableMap()))); // Lol, thanks Pleroma
map.set('relationship', relationship);
map.set('moved', moved);
map.set('patron', patron);

Wyświetl plik

@ -21,6 +21,15 @@ export const guessFqn = account => {
return account.get('acct');
};
export const getBaseURL = account => {
try {
const url = account.get('url');
return new URL(url).origin;
} catch {
return '';
}
};
// user@domain even for local users
export const acctFull = account => (
account.get('fqn') || guessFqn(account)

Wyświetl plik

@ -20,6 +20,10 @@ export const getFeatures = createSelector([
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
richText: v.software === 'Pleroma',
securityAPI: v.software === 'Pleroma',
settingsStore: v.software === 'Pleroma',
accountAliasesAPI: v.software === 'Pleroma',
};
});