pl-fe: Allow to configure per-post interaction policies

Signed-off-by: mkljczk <git@mkljczk.pl>
This commit is contained in:
mkljczk
2024-12-30 00:19:50 +01:00
parent 5223e0d5f5
commit 94c5cae631
8 changed files with 150 additions and 4 deletions

View File

@ -19,9 +19,10 @@ import { saveSettings } from './settings';
import { createStatus } from './statuses';
import type { EditorState } from 'lexical';
import type { Account as BaseAccount, CreateStatusParams, CustomEmoji, Group, MediaAttachment, Status as BaseStatus, Tag, Poll, ScheduledStatus } from 'pl-api';
import type { Account as BaseAccount, CreateStatusParams, CustomEmoji, Group, MediaAttachment, Status as BaseStatus, Tag, Poll, ScheduledStatus, InteractionPolicy } from 'pl-api';
import type { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
import type { Emoji } from 'pl-fe/features/emoji';
import type { Policy, Rule, Scope } from 'pl-fe/features/interaction-policies';
import type { Account } from 'pl-fe/normalizers/account';
import type { Status } from 'pl-fe/normalizers/status';
import type { AppDispatch, RootState } from 'pl-fe/store';
@ -92,6 +93,8 @@ const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const;
const COMPOSE_ADD_SUGGESTED_QUOTE = 'COMPOSE_ADD_SUGGESTED_QUOTE' as const;
const COMPOSE_ADD_SUGGESTED_LANGUAGE = 'COMPOSE_ADD_SUGGESTED_LANGUAGE' as const;
const COMPOSE_INTERACTION_POLICY_OPTION_CHANGE = 'COMPOSE_INTERACTION_POLICY_OPTION_CHANGE' as const;
const getAccount = makeGetAccount();
const messages = defineMessages({
@ -400,6 +403,7 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}) =>
language: compose.language || compose.suggested_language || undefined,
to: explicitAddressing && to.length ? to : undefined,
local_only: !compose.federated,
interaction_policy: ['public', 'unlisted', 'private'].includes(compose.privacy) && compose.interactionPolicy || undefined,
};
if (compose.poll) {
@ -929,6 +933,15 @@ const changeComposeFederated = (composeId: string) => ({
composeId,
});
const changeComposeInteractionPolicyOption = (composeId: string, policy: Policy, rule: Rule, value: Scope[], initial: InteractionPolicy) => ({
type: COMPOSE_INTERACTION_POLICY_OPTION_CHANGE,
composeId,
policy,
rule,
value,
initial,
});
type ComposeAction =
ComposeSetStatusAction
| ReturnType<typeof changeCompose>
@ -980,7 +993,8 @@ type ComposeAction =
| ReturnType<typeof changeMediaOrder>
| ReturnType<typeof addSuggestedQuote>
| ReturnType<typeof addSuggestedLanguage>
| ReturnType<typeof changeComposeFederated>;
| ReturnType<typeof changeComposeFederated>
| ReturnType<typeof changeComposeInteractionPolicyOption>;
export {
COMPOSE_CHANGE,
@ -1034,6 +1048,7 @@ export {
COMPOSE_ADD_SUGGESTED_QUOTE,
COMPOSE_ADD_SUGGESTED_LANGUAGE,
COMPOSE_FEDERATED_CHANGE,
COMPOSE_INTERACTION_POLICY_OPTION_CHANGE,
setComposeToStatus,
replyCompose,
cancelReplyCompose,
@ -1080,6 +1095,7 @@ export {
addSuggestedQuote,
addSuggestedLanguage,
changeComposeFederated,
changeComposeInteractionPolicyOption,
type ComposeReplyAction,
type ComposeSuggestionSelectAction,
type ComposeAction,

View File

@ -31,6 +31,7 @@ import { $createEmojiNode } from '../editor/nodes/emoji-node';
import { countableText } from '../util/counter';
import ContentTypeButton from './content-type-button';
import InteractionPolicyButton from './interaction-policy-button';
import LanguageDropdown from './language-dropdown';
import PollButton from './poll-button';
import PollForm from './polls/poll-form';
@ -186,6 +187,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
{features.polls && <PollButton composeId={id} />}
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
{anyMedia && features.spoilers && <SensitiveMediaButton composeId={id} />}
{features.interactionRequests && <InteractionPolicyButton composeId={id} />}
</HStack>
), [features, id, anyMedia]);

View File

@ -0,0 +1,42 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useCompose } from 'pl-fe/hooks/use-compose';
import { useModalsStore } from 'pl-fe/stores/modals';
import ComposeFormButton from './compose-form-button';
const messages = defineMessages({
label: { id: 'compose_form.interaction_policy.label', defaultMessage: 'Manage interaction policy' },
});
interface IInteractionPolicyButton {
composeId: string;
}
const InteractionPolicyButton: React.FC<IInteractionPolicyButton> = ({ composeId }) => {
const intl = useIntl();
const { openModal } = useModalsStore();
const handleClick = () => {
openModal('COMPOSE_INTERACTION_POLICY', {
composeId,
});
};
const { privacy, interactionPolicy } = useCompose(composeId);
if (!['public', 'unlisted', 'private'].includes(privacy)) return null;
return (
<ComposeFormButton
icon={require('@tabler/icons/outline/adjustments-star.svg')}
title={intl.formatMessage(messages.label)}
onClick={handleClick}
active={!!interactionPolicy}
/>
);
};
export { InteractionPolicyButton as default };

View File

@ -207,4 +207,10 @@ const InteractionPolicies = () => {
);
};
export { InteractionPolicies as default };
export {
InteractionPolicies as default,
InteractionPolicyConfig,
type Policy,
type Rule,
type Scope,
};

View File

@ -15,6 +15,7 @@ const MODAL_COMPONENTS = {
COMPARE_HISTORY: lazy(() => import('pl-fe/features/ui/components/modals/compare-history-modal')),
COMPONENT: lazy(() => import('pl-fe/features/ui/components/modals/component-modal')),
COMPOSE: lazy(() => import('pl-fe/features/ui/components/modals/compose-modal')),
COMPOSE_INTERACTION_POLICY: lazy(() => import('pl-fe/features/ui/components/modals/compose-interaction-policy-modal')),
CONFIRM: lazy(() => import('pl-fe/features/ui/components/modals/confirmation-modal')),
CREATE_GROUP: lazy(() => import('pl-fe/features/ui/components/modals/manage-group-modal')),
CRYPTO_DONATE: lazy(() => import('pl-fe/features/ui/components/modals/crypto-donate-modal')),

View File

@ -0,0 +1,65 @@
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { changeComposeInteractionPolicyOption } from 'pl-fe/actions/compose';
import Modal from 'pl-fe/components/ui/modal';
import Stack from 'pl-fe/components/ui/stack';
import { InteractionPolicyConfig, type Policy, type Rule, type Scope } from 'pl-fe/features/interaction-policies';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useCompose } from 'pl-fe/hooks/use-compose';
import { useInteractionPolicies } from 'pl-fe/queries/settings/use-interaction-policies';
import type { BaseModalProps } from '../modal-root';
interface ComposeInteractionPolicyModalProps {
composeId: string;
}
const ComposeInteractionPolicyModal: React.FC<BaseModalProps & ComposeInteractionPolicyModalProps> = ({ composeId, onClose }) => {
const dispatch = useAppDispatch();
const { interactionPolicies: initial } = useInteractionPolicies();
const compose = useCompose(composeId);
const canManageInteractionPolicies = compose.privacy === 'public' || compose.privacy === 'unlisted' || compose.privacy === 'private';
useEffect(() => {
if (!canManageInteractionPolicies) {
onClose('COMPOSE_INTERACTION_POLICY');
}
}, []);
if (!canManageInteractionPolicies) {
return null;
}
const interactionPolicy = (compose.interactionPolicy || initial[compose.privacy as 'public']);
const onClickClose = () => {
onClose('COMPOSE_INTERACTION_POLICY');
};
const onChange = (policy: Policy, rule: Rule, value: Scope[]) => {
dispatch(changeComposeInteractionPolicyOption(composeId, policy, rule, value, interactionPolicy));
};
return (
<Modal
title={<FormattedMessage id='navigation_bar.interaction_policy' defaultMessage='Status interaction policy' />}
onClose={onClickClose}
closeIcon={composeId === 'compose-modal' ? require('@tabler/icons/outline/arrow-left.svg') : undefined}
closePosition={composeId === 'compose-modal' ? 'left' : undefined}
>
<Stack space={4}>
<InteractionPolicyConfig
interactionPolicy={interactionPolicy}
visibility={compose.privacy as 'public'}
onChange={onChange}
singlePost
/>
</Stack>
</Modal>
);
};
export { ComposeInteractionPolicyModal as default, type ComposeInteractionPolicyModalProps };

View File

@ -1,5 +1,4 @@
import { create } from 'mutative';
import { type CredentialAccount, type Instance, type MediaAttachment, type Tag } from 'pl-api';
import { INSTANCE_FETCH_SUCCESS, type InstanceAction } from 'pl-fe/actions/instance';
import { tagHistory } from 'pl-fe/settings';
@ -58,6 +57,7 @@ import {
COMPOSE_FEDERATED_CHANGE,
type ComposeAction,
type ComposeSuggestionSelectAction,
COMPOSE_INTERACTION_POLICY_OPTION_CHANGE,
} from '../actions/compose';
import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events';
import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, type MeAction } from '../actions/me';
@ -65,6 +65,7 @@ import { FE_NAME } from '../actions/settings';
import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines';
import { unescapeHTML } from '../utils/html';
import type { InteractionPolicy, CredentialAccount, Instance, MediaAttachment, Tag } from 'pl-api';
import type { Emoji } from 'pl-fe/features/emoji';
import type { Language } from 'pl-fe/features/preferences';
import type { Account } from 'pl-fe/normalizers/account';
@ -127,6 +128,7 @@ interface Compose {
suggested_language: string | null;
federated: boolean;
approvalRequired: boolean;
interactionPolicy: InteractionPolicy | null;
}
const newCompose = (params: Partial<Compose> = {}): Compose => ({
@ -167,6 +169,7 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
suggested_language: null,
federated: true,
approvalRequired: false,
interactionPolicy: null,
...params,
});
@ -676,6 +679,15 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
return updateCompose(state, action.composeId, compose => {
compose.federated = !compose.federated;
});
case COMPOSE_INTERACTION_POLICY_OPTION_CHANGE:
return updateCompose(state, action.composeId, compose => {
if (compose.interactionPolicy === null) compose.interactionPolicy = JSON.parse(JSON.stringify(action.initial))!;
compose.interactionPolicy = create(compose.interactionPolicy || action.initial, (interactionPolicy) => {
interactionPolicy[action.policy][action.rule] = action.value;
interactionPolicy[action.policy][action.rule === 'always' ? 'with_approval' : 'always'] = interactionPolicy[action.policy][action.rule === 'always' ? 'with_approval' : 'always'].filter(rule => !action.value.includes(rule as any));
});
});
case INSTANCE_FETCH_SUCCESS:
return updateCompose(state, 'default', (compose) => updateDefaultContentType(compose, action.instance));
default:

View File

@ -9,6 +9,7 @@ import type { AccountModerationModalProps } from 'pl-fe/features/ui/components/m
import type { BoostModalProps } from 'pl-fe/features/ui/components/modals/boost-modal';
import type { CompareHistoryModalProps } from 'pl-fe/features/ui/components/modals/compare-history-modal';
import type { ComponentModalProps } from 'pl-fe/features/ui/components/modals/component-modal';
import type { ComposeInteractionPolicyModalProps } from 'pl-fe/features/ui/components/modals/compose-interaction-policy-modal';
import type { ComposeModalProps } from 'pl-fe/features/ui/components/modals/compose-modal';
import type { ConfirmationModalProps } from 'pl-fe/features/ui/components/modals/confirmation-modal';
import type { DislikesModalProps } from 'pl-fe/features/ui/components/modals/dislikes-modal';
@ -45,6 +46,7 @@ type OpenModalProps =
| [type: 'COMPARE_HISTORY', props: CompareHistoryModalProps]
| [type: 'COMPONENT', props: ComponentModalProps]
| [type: 'COMPOSE', props?: ComposeModalProps]
| [type: 'COMPOSE_INTERACTION_POLICY', props?: ComposeInteractionPolicyModalProps]
| [type: 'CONFIRM', props: ConfirmationModalProps]
| [type: 'CRYPTO_DONATE', props: ICryptoAddress]
| [type: 'DISLIKES', props: DislikesModalProps]