pl-fe: allow redacting posts by admins, when supported by backend
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -1,8 +1,11 @@
|
||||
import { importEntities } from 'pl-fe/actions/importer';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
import { filterBadges, getTagDiff } from 'pl-fe/utils/badges';
|
||||
|
||||
import { getClient } from '../api';
|
||||
|
||||
import { setComposeToStatus } from './compose';
|
||||
import { STATUS_FETCH_SOURCE_FAIL, type StatusesAction } from './statuses';
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
|
||||
import type { PleromaConfig } from 'pl-api';
|
||||
@ -118,6 +121,20 @@ const setRole = (accountId: string, role: 'user' | 'moderator' | 'admin') =>
|
||||
}
|
||||
};
|
||||
|
||||
const redactStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
||||
const status = state.statuses[statusId]!;
|
||||
const poll = status.poll_id ? state.polls[status.poll_id] : undefined;
|
||||
|
||||
return getClient(state).statuses.getStatusSource(statusId).then(response => {
|
||||
dispatch(setComposeToStatus(status, poll, response.text, response.spoiler_text, response.content_type, false, undefined, undefined, true));
|
||||
useModalsStore.getState().openModal('COMPOSE');
|
||||
}).catch(error => {
|
||||
dispatch<StatusesAction>({ type: STATUS_FETCH_SOURCE_FAIL, error });
|
||||
});
|
||||
};
|
||||
|
||||
type AdminActions =
|
||||
| { type: typeof ADMIN_CONFIG_FETCH_SUCCESS; configs: PleromaConfig['configs']; needsReboot: boolean }
|
||||
| { type: typeof ADMIN_CONFIG_UPDATE_REQUEST; configs: PleromaConfig['configs'] }
|
||||
@ -136,5 +153,6 @@ export {
|
||||
toggleStatusSensitivity,
|
||||
setBadges,
|
||||
setRole,
|
||||
redactStatus,
|
||||
type AdminActions,
|
||||
};
|
||||
|
||||
@ -104,12 +104,15 @@ const COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE = 'COMPOSE_CLEAR_LINK_SUGGESTION_IGNO
|
||||
const COMPOSE_HASHTAG_CASING_SUGGESTION_SET = 'COMPOSE_HASHTAG_CASING_SUGGESTION_SET' as const;
|
||||
const COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE = 'COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE' as const;
|
||||
|
||||
const COMPOSE_REDACTING_OVERWRITE_CHANGE = 'COMPOSE_REDACTING_OVERWRITE_CHANGE' as const;
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const messages = defineMessages({
|
||||
scheduleError: { id: 'compose.invalid_schedule', defaultMessage: 'You must schedule a post at least 5 minutes out.' },
|
||||
success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' },
|
||||
editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' },
|
||||
redactSuccess: { id: 'compose.redact_success', defaultMessage: 'The post was redacted' },
|
||||
scheduledSuccess: { id: 'compose.scheduled_success', defaultMessage: 'Your post was scheduled' },
|
||||
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
|
||||
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
|
||||
@ -130,6 +133,7 @@ interface ComposeSetStatusAction {
|
||||
withRedraft?: boolean;
|
||||
draftId?: string;
|
||||
editorState?: string | null;
|
||||
redacting?: boolean;
|
||||
}
|
||||
|
||||
const setComposeToStatus = (
|
||||
@ -141,6 +145,7 @@ const setComposeToStatus = (
|
||||
withRedraft?: boolean,
|
||||
draftId?: string,
|
||||
editorState?: string | null,
|
||||
redacting?: boolean,
|
||||
) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const { features } = getClient(getState);
|
||||
@ -158,6 +163,7 @@ const setComposeToStatus = (
|
||||
withRedraft,
|
||||
draftId,
|
||||
editorState,
|
||||
redacting,
|
||||
});
|
||||
};
|
||||
|
||||
@ -299,7 +305,7 @@ const directComposeById = (accountId: string) =>
|
||||
useModalsStore.getState().openModal('COMPOSE');
|
||||
};
|
||||
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: BaseStatus | ScheduledStatus, status: string, edit?: boolean) => {
|
||||
const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, composeId: string, data: BaseStatus | ScheduledStatus, status: string, edit?: boolean, redact?: boolean) => {
|
||||
if (!dispatch || !getState) return;
|
||||
|
||||
const state = getState();
|
||||
@ -310,7 +316,7 @@ const handleComposeSubmit = (dispatch: AppDispatch, getState: () => RootState, c
|
||||
dispatch(submitComposeSuccess(composeId, data, accountUrl, draftId));
|
||||
if (data.scheduled_at === null) {
|
||||
dispatch(insertIntoTagHistory(composeId, data.tags || [], status));
|
||||
toast.success(edit ? messages.editSuccess : messages.success, {
|
||||
toast.success(redact ? messages.redactSuccess : edit ? messages.editSuccess : messages.success, {
|
||||
actionLabel: messages.view,
|
||||
actionLink: (data.visibility === 'direct' && getClient(getState()).features.conversations) ? '/conversations' : `/@${data.account.acct}/posts/${data.id}`,
|
||||
});
|
||||
@ -456,8 +462,13 @@ const submitCompose = (composeId: string, opts: SubmitComposeOpts = {}, preview
|
||||
onSuccess?.();
|
||||
}).catch(() => {});
|
||||
} else {
|
||||
return dispatch(createStatus(params, idempotencyKey, statusId)).then((data) => {
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId);
|
||||
if (compose.redacting) {
|
||||
// @ts-ignore
|
||||
params.overwrite = compose.redactingOverwrite;
|
||||
}
|
||||
|
||||
return dispatch(createStatus(params, idempotencyKey, statusId, compose.redacting)).then((data) => {
|
||||
handleComposeSubmit(dispatch, getState, composeId, data, status, !!statusId, compose.redacting);
|
||||
onSuccess?.();
|
||||
}).catch((error) => {
|
||||
dispatch(submitComposeFail(composeId, error));
|
||||
@ -998,6 +1009,12 @@ const ignoreHashtagCasingSuggestion = (composeId: string) => ({
|
||||
composeId,
|
||||
});
|
||||
|
||||
const changeComposeRedactingOverwrite = (composeId: string, value: boolean) => ({
|
||||
type: COMPOSE_REDACTING_OVERWRITE_CHANGE,
|
||||
composeId,
|
||||
value,
|
||||
});
|
||||
|
||||
type ComposeAction =
|
||||
ComposeSetStatusAction
|
||||
| ReturnType<typeof changeCompose>
|
||||
@ -1056,7 +1073,8 @@ type ComposeAction =
|
||||
| ReturnType<typeof suggestClearLink>
|
||||
| ReturnType<typeof ignoreClearLinkSuggestion>
|
||||
| ReturnType<typeof suggestHashtagCasing>
|
||||
| ReturnType<typeof ignoreHashtagCasingSuggestion>;
|
||||
| ReturnType<typeof ignoreHashtagCasingSuggestion>
|
||||
| ReturnType<typeof changeComposeRedactingOverwrite>;
|
||||
|
||||
export {
|
||||
COMPOSE_CHANGE,
|
||||
@ -1117,6 +1135,7 @@ export {
|
||||
COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_SET,
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE,
|
||||
COMPOSE_REDACTING_OVERWRITE_CHANGE,
|
||||
setComposeToStatus,
|
||||
replyCompose,
|
||||
cancelReplyCompose,
|
||||
@ -1169,6 +1188,7 @@ export {
|
||||
cancelPreviewCompose,
|
||||
suggestHashtagCasing,
|
||||
ignoreHashtagCasingSuggestion,
|
||||
changeComposeRedactingOverwrite,
|
||||
type ComposeReplyAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
type ComposeAction,
|
||||
|
||||
@ -36,11 +36,19 @@ const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS' as const;
|
||||
|
||||
const STATUS_UNFILTER = 'STATUS_UNFILTER' as const;
|
||||
|
||||
const createStatus = (params: CreateStatusParams, idempotencyKey: string, statusId: string | null) =>
|
||||
const createStatus = (params: CreateStatusParams, idempotencyKey: string, statusId: string | null, redacting = false) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (!params.preview) dispatch<StatusesAction>({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId });
|
||||
if (!params.preview) dispatch<StatusesAction>({ type: STATUS_CREATE_REQUEST, params, idempotencyKey, editing: !!statusId, redacting });
|
||||
|
||||
return (statusId === null ? getClient(getState()).statuses.createStatus(params) : getClient(getState()).statuses.editStatus(statusId, params))
|
||||
const client = getClient(getState());
|
||||
|
||||
return (
|
||||
statusId === null
|
||||
? client.statuses.createStatus(params)
|
||||
: redacting
|
||||
? client.admin.statuses.redactStatus(statusId, params)
|
||||
: client.statuses.editStatus(statusId, params)
|
||||
)
|
||||
.then((status) => {
|
||||
if (params.preview) return status;
|
||||
|
||||
@ -241,7 +249,7 @@ const unfilterStatus = (statusId: string) => ({
|
||||
});
|
||||
|
||||
type StatusesAction =
|
||||
| { type: typeof STATUS_CREATE_REQUEST; params: CreateStatusParams; idempotencyKey: string; editing: boolean }
|
||||
| { type: typeof STATUS_CREATE_REQUEST; params: CreateStatusParams; idempotencyKey: string; editing: boolean; redacting: boolean }
|
||||
| { type: typeof STATUS_CREATE_SUCCESS; status: BaseStatus | ScheduledStatus; params: CreateStatusParams; idempotencyKey: string; editing: boolean }
|
||||
| { type: typeof STATUS_CREATE_FAIL; error: unknown; params: CreateStatusParams; idempotencyKey: string; editing: boolean }
|
||||
| { type: typeof STATUS_FETCH_SOURCE_REQUEST }
|
||||
|
||||
@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
|
||||
import { blockAccount } from 'pl-fe/actions/accounts';
|
||||
import { redactStatus } from 'pl-fe/actions/admin';
|
||||
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'pl-fe/actions/compose';
|
||||
import { emojiReact, unEmojiReact } from 'pl-fe/actions/emoji-reacts';
|
||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'pl-fe/actions/moderation';
|
||||
@ -100,6 +101,7 @@ const messages = defineMessages({
|
||||
reblog_visibility_public: { id: 'status.reblog_visibility_public', defaultMessage: 'Public repost' },
|
||||
reblog_visibility_unlisted: { id: 'status.reblog_visibility_unlisted', defaultMessage: 'Unlisted repost' },
|
||||
reblog_visibility_private: { id: 'status.reblog_visibility_private', defaultMessage: 'Followers-only repost' },
|
||||
redact: { id: 'status.redact', defaultMessage: 'Redact' },
|
||||
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
|
||||
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
|
||||
redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' },
|
||||
@ -825,6 +827,10 @@ const MenuButton: React.FC<IMenuButton> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRedactStatus: React.EventHandler<React.MouseEvent> = () => {
|
||||
dispatch(redactStatus(status.id));
|
||||
};
|
||||
|
||||
const menu: Menu = [];
|
||||
|
||||
if (expandable) {
|
||||
@ -1088,6 +1094,15 @@ const MenuButton: React.FC<IMenuButton> = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (isAdmin && features.pleromaAdminStatusesRedact) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.redact),
|
||||
action: handleRedactStatus,
|
||||
icon: require('@tabler/icons/outline/pencil.svg'),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!ownAccount) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.deleteStatus),
|
||||
|
||||
@ -14,13 +14,16 @@ import {
|
||||
ignoreClearLinkSuggestion,
|
||||
suggestClearLink,
|
||||
resetCompose,
|
||||
changeComposeRedactingOverwrite,
|
||||
} from 'pl-fe/actions/compose';
|
||||
import { saveDraftStatus } from 'pl-fe/actions/draft-statuses';
|
||||
import DropdownMenu from 'pl-fe/components/dropdown-menu';
|
||||
import List, { ListItem } from 'pl-fe/components/list';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import SvgIcon from 'pl-fe/components/ui/svg-icon';
|
||||
import Toggle from 'pl-fe/components/ui/toggle';
|
||||
import EmojiPickerDropdown from 'pl-fe/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { ComposeEditor } from 'pl-fe/features/ui/util/async-components';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
@ -285,6 +288,10 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
dispatch(ignoreClearLinkSuggestion(id, key));
|
||||
};
|
||||
|
||||
const handleChangeRedactingOverwrite: React.ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
dispatch(changeComposeRedactingOverwrite(id, e.target.checked));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClick, true);
|
||||
|
||||
@ -438,6 +445,21 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||
|
||||
<ComposeButton type='submit' icon={publishIcon} text={publishText} disabled={!canSubmit} actionsMenu={actionsMenu} />
|
||||
</HStack>
|
||||
|
||||
{compose.redacting && (
|
||||
<List>
|
||||
<ListItem
|
||||
className='mt-2'
|
||||
label={<FormattedMessage id='compose.redact.overwrite_label' defaultMessage='Overwrite existing status' />}
|
||||
hint={<FormattedMessage id='compose.redact.overwrite_hint' defaultMessage='This will replace the status with a new one, without keeping edit history. The update will not federate.' />}
|
||||
>
|
||||
<Toggle
|
||||
checked={compose.redactingOverwrite}
|
||||
onChange={handleChangeRedactingOverwrite}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@ -505,6 +505,9 @@
|
||||
"compose.language_dropdown.prompt": "Select language",
|
||||
"compose.language_dropdown.search": "Search language…",
|
||||
"compose.language_dropdown.suggestion": "{language} (detected)",
|
||||
"compose.redact.overwrite_hint": "This will replace the status with a new one, without keeping edit history. The update will not federate.",
|
||||
"compose.redact.overwrite_label": "Overwrite existing status",
|
||||
"compose.redact_success": "The post was redacted",
|
||||
"compose.reply_group_indicator.message": "Posting to {groupLink}",
|
||||
"compose.scheduled_success": "Your post was scheduled",
|
||||
"compose.submit_success": "Your post was sent!",
|
||||
@ -1235,6 +1238,7 @@
|
||||
"navigation_bar.compose_group": "Compose to group",
|
||||
"navigation_bar.compose_group_reply": "Reply to group post",
|
||||
"navigation_bar.compose_quote": "Quote post",
|
||||
"navigation_bar.compose_redact": "Redact post",
|
||||
"navigation_bar.compose_reply": "Reply to post",
|
||||
"navigation_bar.create_event": "Create new event",
|
||||
"navigation_bar.create_group": "Create group",
|
||||
@ -1705,6 +1709,7 @@
|
||||
"status.reblogged_by_private": "{name} reposted to followers",
|
||||
"status.reblogged_by_with_group": "{name} reposted from {group}",
|
||||
"status.reblogs.empty": "No one has reposted this post yet. When someone does, they will show up here.",
|
||||
"status.redact": "Redact",
|
||||
"status.redraft": "Delete & re-draft",
|
||||
"status.remove_account_from_group": "Remove account from group",
|
||||
"status.remove_post_from_group": "Remove post from group",
|
||||
|
||||
@ -70,8 +70,9 @@ const ComposeModal: React.FC<BaseModalProps & ComposeModalProps> = ({ onClose, c
|
||||
const renderTitle = () => {
|
||||
if (compose.draft_id) {
|
||||
return <FormattedMessage id='navigation_bar.compose_draft' defaultMessage='Edit draft post' />;
|
||||
}
|
||||
if (statusId) {
|
||||
} else if (compose.redacting) {
|
||||
return <FormattedMessage id='navigation_bar.compose_redact' defaultMessage='Redact post' />;
|
||||
} else if (statusId) {
|
||||
return <FormattedMessage id='navigation_bar.compose_edit' defaultMessage='Edit post' />;
|
||||
} else if (privacy === 'direct') {
|
||||
return <FormattedMessage id='navigation_bar.compose_direct' defaultMessage='Direct message' />;
|
||||
|
||||
@ -64,6 +64,7 @@ import {
|
||||
COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE,
|
||||
type ComposeAction,
|
||||
type ComposeSuggestionSelectAction,
|
||||
COMPOSE_REDACTING_OVERWRITE_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';
|
||||
@ -146,6 +147,8 @@ interface Compose {
|
||||
preview: Partial<BaseStatus> | null;
|
||||
hashtag_casing_suggestion: string | null;
|
||||
hashtag_casing_suggestion_ignored: boolean | null;
|
||||
redacting: boolean;
|
||||
redactingOverwrite: boolean;
|
||||
}
|
||||
|
||||
const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
@ -192,6 +195,8 @@ const newCompose = (params: Partial<Compose> = {}): Compose => ({
|
||||
preview: null,
|
||||
hashtag_casing_suggestion: null,
|
||||
hashtag_casing_suggestion_ignored: null,
|
||||
redacting: false,
|
||||
redactingOverwrite: false,
|
||||
...params,
|
||||
});
|
||||
|
||||
@ -569,6 +574,8 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
||||
compose.media_attachments = action.status.media_attachments;
|
||||
compose.sensitive = action.status.sensitive;
|
||||
|
||||
compose.redacting = action.redacting || false;
|
||||
|
||||
if (action.status.spoiler_text.length > 0) {
|
||||
compose.spoiler_text = action.status.spoiler_text;
|
||||
} else {
|
||||
@ -749,6 +756,10 @@ const compose = (state = initialState, action: ComposeAction | EventsAction | In
|
||||
compose.hashtag_casing_suggestion = null;
|
||||
compose.hashtag_casing_suggestion_ignored = true;
|
||||
});
|
||||
case COMPOSE_REDACTING_OVERWRITE_CHANGE:
|
||||
return updateCompose(state, action.composeId, compose => {
|
||||
compose.redactingOverwrite = action.value;
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user