nicolium: support quote approval policy on mastodon

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-02-18 15:37:24 +01:00
parent 1528bcbe00
commit 224a221647
6 changed files with 39 additions and 9 deletions

View File

@@ -277,10 +277,12 @@ interface ComposeQuoteAction {
account: Pick<Account, 'acct'> | undefined;
explicitAddressing: boolean;
conversationScope: boolean;
approvalRequired?: boolean;
}
const quoteCompose =
(status: ComposeQuoteAction['status']) => (dispatch: AppDispatch, getState: () => RootState) => {
(status: ComposeQuoteAction['status'], approvalRequired?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
const { forceImplicitAddressing } = useSettingsStore.getState().settings;
const { createStatusConversationScope, createStatusExplicitAddressing } =
@@ -294,6 +296,7 @@ const quoteCompose =
account: selectOwnAccount(state),
explicitAddressing,
conversationScope: createStatusConversationScope,
approvalRequired,
});
useModalsStore.getState().actions.openModal('COMPOSE');
};

View File

@@ -26,6 +26,7 @@ type MenuItem = {
items?: Menu;
onSelectFile?: (files: FileList) => void;
accept?: string;
disabled?: boolean;
} & (LinkOptions | { to?: undefined });
interface IDropdownMenuItem {
@@ -46,6 +47,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
event.stopPropagation();
if (!item) return;
if (item.disabled) return;
if (item.items?.length) {
event.preventDefault();
@@ -74,6 +76,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
const handleAuxClick: React.EventHandler<React.MouseEvent> = (event) => {
if (!item) return;
if (item.disabled) return;
if (item.onSelectFile) fileElement.current?.click();
if (onClick) onClick();
@@ -93,6 +96,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
event.stopPropagation();
if (!item) return;
if (item.disabled) return;
if (item.onChange) item.onChange(event.target.checked);
};
@@ -132,6 +136,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
'mx-2 my-1 flex cursor-pointer items-center rounded-md px-2 py-1.5 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 focus:outline-none black:hover:bg-gray-900 black:focus:bg-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus:bg-gray-800 dark:focus:text-gray-200',
{
'text-danger-600 dark:text-danger-400': item.destructive,
'cursor-not-allowed opacity-50': item.disabled,
},
)}
>
@@ -185,6 +190,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
accept={item.accept}
onChange={handleSelectFileChange}
className='hidden'
disabled={item.disabled}
/>
</label>
)}

View File

@@ -421,6 +421,7 @@ const ReblogButton: React.FC<IReblogButton> = ({
const { boostModal } = useSettings();
const { openModal } = useModalsActions();
const canReblog = useCanInteract(status, 'can_reblog');
const canQuote = useCanInteract(status, 'can_quote');
const { mutate: reblogStatus } = useReblogStatus(status.id);
const { mutate: unreblogStatus } = useUnreblogStatus(status.id);
@@ -494,7 +495,7 @@ const ReblogButton: React.FC<IReblogButton> = ({
const handleQuoteClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(quoteCompose(status));
dispatch(quoteCompose(status, canQuote.approvalRequired || false));
} else {
onOpenUnauthorizedModal('REBLOG');
}
@@ -510,6 +511,7 @@ const ReblogButton: React.FC<IReblogButton> = ({
text: intl.formatMessage(messages.quotePost),
action: handleQuoteClick,
icon: require('@phosphor-icons/core/regular/quotes.svg'),
disabled: !canQuote.canInteract,
},
];

View File

@@ -426,13 +426,20 @@ const ComposeForm = <ID extends string>({
onClick={handleClick}
onSubmit={handleSubmit}
>
{!!compose.inReplyToId && compose.approvalRequired && (
{(compose.inReplyToId || compose.quoteId) && compose.approvalRequired && (
<Warning
message={
<FormattedMessage
id='compose_form.approval_required'
defaultMessage='The reply needs to be approved by the post author.'
/>
compose.quoteId ? (
<FormattedMessage
id='compose_form.approval_required.quote'
defaultMessage='The quote needs to be approved by the post author.'
/>
) : (
<FormattedMessage
id='compose_form.approval_required'
defaultMessage='The reply needs to be approved by the post author.'
/>
)
}
/>
)}

View File

@@ -6,8 +6,11 @@ import type { MinifiedStatus } from '@/reducers/statuses';
import type { InteractionPolicy, InteractionPolicyEntry } from 'pl-api';
const useCanInteract = (
status: Pick<MinifiedStatus, 'account_id' | 'id' | 'interaction_policy' | 'mentions'>,
type: keyof InteractionPolicy,
status: Pick<
MinifiedStatus,
'account_id' | 'id' | 'interaction_policy' | 'mentions' | 'quote_approval'
>,
type: keyof InteractionPolicy | 'can_quote',
): {
canInteract: boolean;
approvalRequired: boolean | null;
@@ -16,6 +19,14 @@ const useCanInteract = (
const me = useAppSelector((state) => state.me);
return useMemo(() => {
if (type === 'can_quote') {
const quoteApproval = status.quote_approval;
return {
canInteract: !quoteApproval || quoteApproval.current_user !== 'denied',
approvalRequired: quoteApproval?.current_user === 'manual',
};
}
const interactionPolicy = status.interaction_policy;
if (me === status.account_id || interactionPolicy[type].always.includes('me'))

View File

@@ -509,6 +509,7 @@ const compose = (
compose.caretPosition = null;
compose.contentType = defaultCompose.contentType;
compose.spoilerText = '';
compose.approvalRequired = action.approvalRequired ?? false;
if (action.status.visibility === 'group') {
compose.groupId = action.status.group_id;