Merge pull request #17 from mkljczk/statuses-visibility
This commit is contained in:
commit
9979932a43
15 changed files with 175 additions and 51 deletions
|
@ -76,10 +76,10 @@ const getAuthApp = () =>
|
|||
const createAuthApp = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const params = {
|
||||
client_name: sourceCode.displayName,
|
||||
client_name: sourceCode.displayName,
|
||||
redirect_uris: 'urn:ietf:wg:oauth:2.0:oob',
|
||||
scopes: getScopes(getState()),
|
||||
website: sourceCode.homepage,
|
||||
scopes: getScopes(getState()),
|
||||
website: sourceCode.homepage,
|
||||
};
|
||||
|
||||
return dispatch(createApp(params)).then((app: Record<string, string>) =>
|
||||
|
|
|
@ -65,7 +65,7 @@ const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE' as const;
|
|||
const COMPOSE_MODIFIED_LANGUAGE_CHANGE = 'COMPOSE_MODIFIED_LANGUAGE_CHANGE' as const;
|
||||
const COMPOSE_LANGUAGE_ADD = 'COMPOSE_LANGUAGE_ADD' as const;
|
||||
const COMPOSE_LANGUAGE_DELETE = 'COMPOSE_LANGUAGE_DELETE' as const;
|
||||
const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE' as const;
|
||||
const COMPOSE_FEDERATED_CHANGE = 'COMPOSE_FEDERATED_CHANGE' as const;
|
||||
|
||||
const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT' as const;
|
||||
|
||||
|
@ -387,6 +387,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
|
|||
scheduled_at: compose.schedule,
|
||||
language: compose.language || compose.suggested_language,
|
||||
to,
|
||||
federated: compose.federated,
|
||||
};
|
||||
|
||||
if (compose.language && compose.textMap.size) {
|
||||
|
@ -937,6 +938,11 @@ const addSuggestedLanguage = (composeId: string, language: string) => ({
|
|||
language,
|
||||
});
|
||||
|
||||
const changeComposeFederated = (composeId: string) => ({
|
||||
type: COMPOSE_FEDERATED_CHANGE,
|
||||
id: composeId,
|
||||
});
|
||||
|
||||
type ComposeAction =
|
||||
ComposeSetStatusAction
|
||||
| ReturnType<typeof changeCompose>
|
||||
|
@ -989,6 +995,7 @@ type ComposeAction =
|
|||
| ReturnType<typeof changeMediaOrder>
|
||||
| ReturnType<typeof addSuggestedQuote>
|
||||
| ReturnType<typeof addSuggestedLanguage>
|
||||
| ReturnType<typeof changeComposeFederated>
|
||||
|
||||
export {
|
||||
COMPOSE_CHANGE,
|
||||
|
@ -1022,7 +1029,6 @@ export {
|
|||
COMPOSE_MODIFIED_LANGUAGE_CHANGE,
|
||||
COMPOSE_LANGUAGE_ADD,
|
||||
COMPOSE_LANGUAGE_DELETE,
|
||||
COMPOSE_LISTABILITY_CHANGE,
|
||||
COMPOSE_EMOJI_INSERT,
|
||||
COMPOSE_UPLOAD_CHANGE_REQUEST,
|
||||
COMPOSE_UPLOAD_CHANGE_SUCCESS,
|
||||
|
@ -1043,6 +1049,7 @@ export {
|
|||
COMPOSE_CHANGE_MEDIA_ORDER,
|
||||
COMPOSE_ADD_SUGGESTED_QUOTE,
|
||||
COMPOSE_ADD_SUGGESTED_LANGUAGE,
|
||||
COMPOSE_FEDERATED_CHANGE,
|
||||
setComposeToStatus,
|
||||
changeCompose,
|
||||
replyCompose,
|
||||
|
@ -1104,5 +1111,6 @@ export {
|
|||
changeMediaOrder,
|
||||
addSuggestedQuote,
|
||||
addSuggestedLanguage,
|
||||
changeComposeFederated,
|
||||
type ComposeAction,
|
||||
};
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
* @see module:soapbox/actions/auth
|
||||
*/
|
||||
|
||||
import api from 'soapbox/api';
|
||||
import toast from 'soapbox/toast';
|
||||
import { getLoggedInAccount } from 'soapbox/utils/auth';
|
||||
import { GOTOSOCIAL, parseVersion } from 'soapbox/utils/features';
|
||||
import { normalizeUsername } from 'soapbox/utils/input';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { AUTH_LOGGED_OUT, messages } from './auth';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
@ -69,20 +69,39 @@ const revokeOAuthTokenById = (id: number) =>
|
|||
const changePassword = (oldPassword: string, newPassword: string, confirmation: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CHANGE_PASSWORD_REQUEST });
|
||||
return api(getState)('/api/pleroma/change_password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
password: oldPassword,
|
||||
new_password: newPassword,
|
||||
new_password_confirmation: confirmation,
|
||||
}),
|
||||
}).then(response => {
|
||||
if (response.json.error) throw response.json.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: CHANGE_PASSWORD_SUCCESS, response });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true });
|
||||
throw error;
|
||||
});
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const v = parseVersion(instance.version);
|
||||
|
||||
if (v.software === GOTOSOCIAL) {
|
||||
return api(getState)('/api/v1/user/password_change', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword,
|
||||
}),
|
||||
}).then(response => {
|
||||
dispatch({ type: CHANGE_PASSWORD_SUCCESS, response });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true });
|
||||
throw error;
|
||||
});
|
||||
} else {
|
||||
return api(getState)('/api/pleroma/change_password', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
password: oldPassword,
|
||||
new_password: newPassword,
|
||||
new_password_confirmation: confirmation,
|
||||
}),
|
||||
}).then(response => {
|
||||
if (response.json.error) throw response.json.error; // This endpoint returns HTTP 200 even on failure
|
||||
dispatch({ type: CHANGE_PASSWORD_SUCCESS, response });
|
||||
}).catch(error => {
|
||||
dispatch({ type: CHANGE_PASSWORD_FAIL, error, skipAlert: true });
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const resetPassword = (usernameOrEmail: string) =>
|
||||
|
@ -110,10 +129,14 @@ const resetPassword = (usernameOrEmail: string) =>
|
|||
const changeEmail = (email: string, password: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CHANGE_EMAIL_REQUEST, email });
|
||||
return api(getState)('/api/pleroma/change_email', {
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const v = parseVersion(instance.version);
|
||||
|
||||
return api(getState)(v.software === GOTOSOCIAL ? '/api/v1/user/email_change' : '/api/pleroma/change_email', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
[v.software === GOTOSOCIAL ? 'new_email' : 'email']: email,
|
||||
password,
|
||||
}),
|
||||
}).then(response => {
|
||||
|
@ -127,10 +150,14 @@ const changeEmail = (email: string, password: string) =>
|
|||
|
||||
const deleteAccount = (password: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch({ type: CHANGE_PASSWORD_REQUEST });
|
||||
const account = getLoggedInAccount(getState());
|
||||
const state = getState();
|
||||
const instance = state.instance;
|
||||
const v = parseVersion(instance.version);
|
||||
|
||||
dispatch({ type: DELETE_ACCOUNT_REQUEST });
|
||||
return api(getState)('/api/pleroma/delete_account', {
|
||||
return api(getState)(v.software === GOTOSOCIAL ? '/api/v1/accounts/delete' : '/api/pleroma/delete_account', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password }),
|
||||
}).then(response => {
|
||||
|
|
|
@ -463,7 +463,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
icon: status.pinned ? require('@tabler/icons/outline/pinned-off.svg') : require('@tabler/icons/outline/pin.svg'),
|
||||
});
|
||||
} else {
|
||||
if (status.visibility === 'private') {
|
||||
if (status.visibility === 'private' || status.visibility === 'mutuals_only') {
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog_private),
|
||||
action: handleReblogClick,
|
||||
|
@ -656,7 +656,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
|
||||
if (status.visibility === 'direct') {
|
||||
reblogIcon = require('@tabler/icons/outline/mail.svg');
|
||||
} else if (status.visibility === 'private') {
|
||||
} else if (status.visibility === 'private' || status.visibility === 'mutuals_only') {
|
||||
reblogIcon = require('@tabler/icons/outline/lock.svg');
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ const LoginPage = () => {
|
|||
<LoginForm handleSubmit={handleSubmit} isLoading={isLoading} />
|
||||
<ConsumersList />
|
||||
|
||||
<div className={'flex items-center gap-2.5 before:flex-1 before:border-b before:border-gray-300 before:content-[\'\'] after:flex-1 after:border-b after:border-gray-300 after:content-[\'\'] before:black:border-gray-800 after:black:border-gray-800 before:dark:border-gray-600 after:dark:border-gray-600'}>
|
||||
<div className={'flex items-center gap-2.5 before:flex-1 before:border-b before:border-gray-300 before:content-[\'\'] after:flex-1 after:border-b after:border-gray-300 after:content-[\'\'] before:dark:border-gray-800 after:dark:border-gray-800'}>
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='login_form.divider' defaultMessage='or' />
|
||||
</Text>
|
||||
|
|
|
@ -229,7 +229,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
} else if (privacy === 'direct') {
|
||||
publishIcon = require('@tabler/icons/outline/mail.svg');
|
||||
publishText = intl.formatMessage(messages.message);
|
||||
} else if (privacy === 'private') {
|
||||
} else if (privacy === 'private' || privacy === 'mutuals_only') {
|
||||
publishIcon = require('@tabler/icons/outline/lock.svg');
|
||||
publishText = intl.formatMessage(messages.publish);
|
||||
} else {
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import clsx from 'clsx';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { spring } from 'react-motion';
|
||||
// @ts-ignore
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
|
||||
import { changeComposeVisibility } from 'soapbox/actions/compose';
|
||||
import { changeComposeFederated, changeComposeVisibility } from 'soapbox/actions/compose';
|
||||
import { closeModal, openModal } from 'soapbox/actions/modals';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
||||
import { Button, Toggle } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useCompose, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
import { userTouching } from 'soapbox/is-mobile';
|
||||
import { GOTOSOCIAL, parseVersion, PLEROMA } from 'soapbox/utils/features';
|
||||
|
||||
import Motion from '../../ui/util/optional-motion';
|
||||
|
||||
|
@ -22,13 +23,26 @@ const messages = defineMessages({
|
|||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Do not post to public timelines' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' },
|
||||
mutuals_only_short: { id: 'privacy.mutuals_only.short', defaultMessage: 'Mutuals-only' },
|
||||
mutuals_only_long: { id: 'privacy.mutuals_only.long', defaultMessage: 'Post to mutually followed users only' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' },
|
||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
|
||||
local_short: { id: 'privacy.local.short', defaultMessage: 'Local-only' },
|
||||
local_long: { id: 'privacy.local.long', defaultMessage: 'Only visible on your instance' },
|
||||
|
||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust post privacy' },
|
||||
local: { id: 'privacy.local', defaultMessage: '{privacy} (local-only)' },
|
||||
});
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
interface Option {
|
||||
icon: string;
|
||||
value: string;
|
||||
text: string;
|
||||
meta: string;
|
||||
}
|
||||
|
||||
interface IPrivacyDropdownMenu {
|
||||
style?: React.CSSProperties;
|
||||
items: any[];
|
||||
|
@ -37,9 +51,14 @@ interface IPrivacyDropdownMenu {
|
|||
onClose: () => void;
|
||||
onChange: (value: string | null) => void;
|
||||
unavailable?: boolean;
|
||||
showFederated?: boolean;
|
||||
federated?: boolean;
|
||||
onChangeFederated: () => void;
|
||||
}
|
||||
|
||||
const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, placement, value, onClose, onChange }) => {
|
||||
const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({
|
||||
style, items, placement, value, onClose, onChange, showFederated, federated, onChangeFederated,
|
||||
}) => {
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const focusedItem = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -52,9 +71,7 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
|
|||
};
|
||||
|
||||
const handleKeyDown: React.KeyboardEventHandler = e => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
const index = items.findIndex(item => item.value === value);
|
||||
let element: ChildNode | null | undefined = null;
|
||||
const index = [...e.currentTarget.parentElement!.children].indexOf(e.currentTarget); let element: ChildNode | null | undefined = null;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
|
@ -86,7 +103,8 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
|
|||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
onChange((element as HTMLElement).getAttribute('data-index'));
|
||||
const value = (element as HTMLElement).getAttribute('data-index');
|
||||
if (value !== 'local_switch') onChange(value);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
@ -97,8 +115,11 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
|
|||
|
||||
e.preventDefault();
|
||||
|
||||
onClose();
|
||||
onChange(value);
|
||||
if (value === 'local_switch') onChangeFederated();
|
||||
else {
|
||||
onClose();
|
||||
onChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -143,7 +164,7 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
|
|||
onKeyDown={handleKeyDown}
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
'flex cursor-pointer p-2.5 text-sm text-gray-700 hover:bg-gray-100 black:hover:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800',
|
||||
'flex cursor-pointer items-center p-2.5 text-gray-700 hover:bg-gray-100 black:hover:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800',
|
||||
{ 'bg-gray-100 dark:bg-gray-800 black:bg-gray-900 hover:bg-gray-200 dark:hover:bg-gray-700': active },
|
||||
)}
|
||||
aria-selected={active}
|
||||
|
@ -154,16 +175,41 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
|
|||
</div>
|
||||
|
||||
<div
|
||||
className={clsx('flex-auto text-primary-600 dark:text-primary-400', {
|
||||
className={clsx('flex-auto text-xs text-primary-600 dark:text-primary-400', {
|
||||
'text-black dark:text-white': active,
|
||||
})}
|
||||
>
|
||||
<strong className='block font-medium text-black dark:text-white'>{item.text}</strong>
|
||||
<strong className='block text-sm font-medium text-black dark:text-white'>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{showFederated && (
|
||||
<div
|
||||
role='option'
|
||||
tabIndex={0}
|
||||
data-index='local_switch'
|
||||
onKeyDown={handleKeyDown}
|
||||
onClick={onChangeFederated}
|
||||
className='flex cursor-pointer items-center p-2.5 text-xs text-gray-700 hover:bg-gray-100 focus:bg-gray-100 black:hover:bg-gray-900 black:focus:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:focus:bg-gray-800'
|
||||
>
|
||||
<div className='mr-2.5 flex items-center justify-center rtl:ml-2.5 rtl:mr-0'>
|
||||
<Icon src={require('@tabler/icons/outline/affiliate.svg')} />
|
||||
</div>
|
||||
|
||||
<div
|
||||
className='flex-auto text-xs text-primary-600 dark:text-primary-400'
|
||||
>
|
||||
<strong className='block text-sm font-medium text-black focus:text-black dark:text-white dark:focus:text-primary-400'>
|
||||
<FormattedMessage id='privacy.local.short' defaultMessage='Local-only' />
|
||||
</strong>
|
||||
<FormattedMessage id='privacy.local.long' defaultMessage='Only visible on your instance' />
|
||||
</div>
|
||||
|
||||
<Toggle checked={!federated} onChange={onChangeFederated} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
|
@ -181,6 +227,10 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
|||
const intl = useIntl();
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
const activeElement = useRef<HTMLElement | null>(null);
|
||||
const instance = useInstance();
|
||||
const features = useFeatures();
|
||||
|
||||
const v = parseVersion(instance.version);
|
||||
|
||||
const compose = useCompose(composeId);
|
||||
|
||||
|
@ -194,11 +244,15 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
|||
{ icon: require('@tabler/icons/outline/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) },
|
||||
{ icon: require('@tabler/icons/outline/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) },
|
||||
{ icon: require('@tabler/icons/outline/lock.svg'), value: 'private', text: intl.formatMessage(messages.private_short), meta: intl.formatMessage(messages.private_long) },
|
||||
features.mutualsOnlyStatuses ? { icon: require('@tabler/icons/outline/users-group.svg'), value: 'mutuals_only', text: intl.formatMessage(messages.mutuals_only_short), meta: intl.formatMessage(messages.mutuals_only_long) } : undefined,
|
||||
{ icon: require('@tabler/icons/outline/mail.svg'), value: 'direct', text: intl.formatMessage(messages.direct_short), meta: intl.formatMessage(messages.direct_long) },
|
||||
];
|
||||
features.localOnlyStatuses && v.software === PLEROMA ? { icon: require('@tabler/icons/outline/affiliate.svg'), value: 'local', text: intl.formatMessage(messages.local_short), meta: intl.formatMessage(messages.local_long) } : undefined,
|
||||
].filter((option): option is Option => !!option);
|
||||
|
||||
const onChange = (value: string | null) => value && dispatch(changeComposeVisibility(composeId, value));
|
||||
|
||||
const onChangeFederated = () => dispatch(changeComposeFederated(composeId));
|
||||
|
||||
const onModalOpen = (props: Record<string, any>) => dispatch(openModal('ACTIONS', props));
|
||||
|
||||
const onModalClose = () => dispatch(closeModal('ACTIONS'));
|
||||
|
@ -280,7 +334,9 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
|||
<Button
|
||||
theme='muted'
|
||||
size='xs'
|
||||
text={valueOption?.text}
|
||||
text={compose.federated ? valueOption?.text : intl.formatMessage(messages.local, {
|
||||
privacy: valueOption?.text,
|
||||
})}
|
||||
icon={valueOption?.icon}
|
||||
secondaryIcon={require('@tabler/icons/outline/chevron-down.svg')}
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
|
@ -297,6 +353,9 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
|||
onClose={handleClose}
|
||||
onChange={onChange}
|
||||
placement={placement}
|
||||
showFederated={features.localOnlyStatuses && v.software === GOTOSOCIAL}
|
||||
federated={compose.federated}
|
||||
onChangeFederated={onChangeFederated}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,7 @@ interface IWarning {
|
|||
const Warning: React.FC<IWarning> = ({ message }) => (
|
||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
||||
{({ opacity, scaleX, scaleY }) => (
|
||||
<div className='mb-2.5 rounded bg-accent-300 px-2.5 py-2 text-xs text-white shadow-md' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
<div className='mb-2.5 rounded border border-solid border-gray-400 bg-transparent px-2.5 py-2 text-xs text-gray-900 dark:border-gray-800 dark:text-white' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -16,7 +16,7 @@ interface IWarningWrapper {
|
|||
const WarningWrapper: React.FC<IWarningWrapper> = ({ composeId }) => {
|
||||
const compose = useCompose(composeId);
|
||||
|
||||
const needsLockWarning = useAppSelector((state) => compose.privacy === 'private' && !selectOwnAccount(state)!.locked);
|
||||
const needsLockWarning = useAppSelector((state) => (compose.privacy === 'private' || compose.privacy === 'mutuals_only') && !selectOwnAccount(state)!.locked);
|
||||
const hashtagWarning = (compose.privacy !== 'public' && compose.privacy !== 'group') && APPROX_HASHTAG_RE.test(compose.text);
|
||||
const directMessageWarning = compose.privacy === 'direct';
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
|||
|
||||
if (actualStatus.visibility === 'direct') {
|
||||
statusTypeIcon = <Icon className='h-4 w-4 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/mail.svg')} />;
|
||||
} else if (actualStatus.visibility === 'private') {
|
||||
} else if (actualStatus.visibility === 'private' || actualStatus.visibility === 'mutuals_only') {
|
||||
statusTypeIcon = <Icon className='h-4 w-4 text-gray-700 dark:text-gray-600' src={require('@tabler/icons/outline/lock.svg')} />;
|
||||
}
|
||||
|
||||
|
|
|
@ -1199,6 +1199,11 @@
|
|||
"privacy.change": "Adjust post privacy",
|
||||
"privacy.direct.long": "Post to mentioned users only",
|
||||
"privacy.direct.short": "Direct",
|
||||
"privacy.local": "{privacy} (local-only)",
|
||||
"privacy.local.long": "Only visible on your instance",
|
||||
"privacy.local.short": "Local-only",
|
||||
"privacy.mutuals_only.long": "Post to mutually followed users only",
|
||||
"privacy.mutuals_only.short": "Mutuals-only",
|
||||
"privacy.private.long": "Post to followers only",
|
||||
"privacy.private.short": "Followers-only",
|
||||
"privacy.public.long": "Post to public timelines",
|
||||
|
|
|
@ -1301,7 +1301,7 @@
|
|||
"security.fields.new_password.label": "Nowe hasło",
|
||||
"security.fields.old_password.label": "Obecne hasło",
|
||||
"security.fields.password.label": "Hasło",
|
||||
"security.fields.password_confirmation.label": "Obecne hasło (ponownie)",
|
||||
"security.fields.password_confirmation.label": "Nowe hasło (ponownie)",
|
||||
"security.headers.delete": "Usuń konto",
|
||||
"security.headers.tokens": "Sesje",
|
||||
"security.qr.fail": "Nie udało się uzyskać klucza konfiguracyjnego",
|
||||
|
|
|
@ -20,7 +20,7 @@ import { maybeFromJS } from 'soapbox/utils/normalizers';
|
|||
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity, EmojiReaction } from 'soapbox/types/entities';
|
||||
|
||||
type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
|
||||
type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group';
|
||||
type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group' | 'mutuals_only' | 'local';
|
||||
|
||||
type EventJoinMode = 'free' | 'restricted' | 'invite';
|
||||
type EventJoinState = 'pending' | 'reject' | 'accept';
|
||||
|
|
|
@ -59,6 +59,7 @@ import {
|
|||
COMPOSE_EDITOR_STATE_SET,
|
||||
COMPOSE_CHANGE_MEDIA_ORDER,
|
||||
COMPOSE_ADD_SUGGESTED_QUOTE,
|
||||
COMPOSE_FEDERATED_CHANGE,
|
||||
ComposeAction,
|
||||
} from '../actions/compose';
|
||||
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
|
||||
|
@ -124,6 +125,7 @@ const ReducerCompose = ImmutableRecord({
|
|||
language: null as Language | null,
|
||||
modified_language: null as Language | null,
|
||||
suggested_language: null as string | null,
|
||||
federated: true,
|
||||
});
|
||||
|
||||
type State = ImmutableMap<string, Compose>;
|
||||
|
@ -226,7 +228,7 @@ const insertEmoji = (compose: Compose, position: number, emojiData: Emoji, needs
|
|||
};
|
||||
|
||||
const privacyPreference = (a: string, b: string) => {
|
||||
const order = ['public', 'unlisted', 'private', 'direct'];
|
||||
const order = ['public', 'unlisted', 'mutuals_only', 'private', 'direct', 'local'];
|
||||
|
||||
if (a === 'group') return a;
|
||||
|
||||
|
@ -602,6 +604,8 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | Me
|
|||
return updateCompose(state, action.id, compose => compose
|
||||
.update('dismissed_quotes', quotes => compose.quote ? quotes.add(compose.quote) : quotes)
|
||||
.set('quote', null));
|
||||
case COMPOSE_FEDERATED_CHANGE:
|
||||
return updateCompose(state, action.id, compose => compose.update('federated', value => !value));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -634,6 +634,15 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
v.software === GOTOSOCIAL,
|
||||
]),
|
||||
|
||||
/**
|
||||
* Ability to post statuses that don't federate.
|
||||
* @see POST /api/v1/statuses
|
||||
*/
|
||||
localOnlyStatuses: federation.enabled && any([
|
||||
v.software === GOTOSOCIAL,
|
||||
v.software === PLEROMA,
|
||||
]),
|
||||
|
||||
/**
|
||||
* Can sign in using username instead of e-mail address.
|
||||
*/
|
||||
|
@ -699,6 +708,12 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
v.software === GOTOSOCIAL,
|
||||
]),
|
||||
|
||||
/**
|
||||
* Ability to post statuses only to accounts with mutual relationship.
|
||||
* @see POST /api/v1/statuses
|
||||
*/
|
||||
mutualsOnlyStatuses: v.software === GOTOSOCIAL,
|
||||
|
||||
/**
|
||||
* Add private notes to accounts.
|
||||
* @see POST /api/v1/accounts/:id/note
|
||||
|
@ -877,8 +892,14 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
* @see POST /api/pleroma/change_password
|
||||
* @see POST /api/pleroma/change_email
|
||||
* @see POST /api/pleroma/delete_account
|
||||
* @see POST /api/v1/user/email_change
|
||||
* @see POST /api/v1/user/password_change
|
||||
* @see POST /api/v1/accounts/delete_account
|
||||
*/
|
||||
security: v.software === PLEROMA,
|
||||
security: any([
|
||||
v.software === PLEROMA,
|
||||
v.software === GOTOSOCIAL,
|
||||
]),
|
||||
|
||||
/**
|
||||
* Ability to manage account sessions.
|
||||
|
|
Loading…
Reference in a new issue