Add ability for owners to delete statuses from Group

This commit is contained in:
Chewbacca 2023-05-02 16:15:14 -04:00
parent b608095e84
commit 4bc92f3c27
5 changed files with 44 additions and 94 deletions

View file

@ -4,7 +4,6 @@ import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedGroups, importFetchedAccounts } from './importer';
import { deleteFromTimelines } from './timelines';
import type { AxiosError } from 'axios';
import type { GroupRole } from 'soapbox/reducers/group-memberships';
@ -35,10 +34,6 @@ const GROUP_RELATIONSHIPS_FETCH_REQUEST = 'GROUP_RELATIONSHIPS_FETCH_REQUEST';
const GROUP_RELATIONSHIPS_FETCH_SUCCESS = 'GROUP_RELATIONSHIPS_FETCH_SUCCESS';
const GROUP_RELATIONSHIPS_FETCH_FAIL = 'GROUP_RELATIONSHIPS_FETCH_FAIL';
const GROUP_DELETE_STATUS_REQUEST = 'GROUP_DELETE_STATUS_REQUEST';
const GROUP_DELETE_STATUS_SUCCESS = 'GROUP_DELETE_STATUS_SUCCESS';
const GROUP_DELETE_STATUS_FAIL = 'GROUP_DELETE_STATUS_FAIL';
const GROUP_KICK_REQUEST = 'GROUP_KICK_REQUEST';
const GROUP_KICK_SUCCESS = 'GROUP_KICK_SUCCESS';
const GROUP_KICK_FAIL = 'GROUP_KICK_FAIL';
@ -206,36 +201,6 @@ const fetchGroupRelationshipsFail = (error: AxiosError) => ({
skipNotFound: true,
});
const groupDeleteStatus = (groupId: string, statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupDeleteStatusRequest(groupId, statusId));
return api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`)
.then(() => {
dispatch(deleteFromTimelines(statusId));
dispatch(groupDeleteStatusSuccess(groupId, statusId));
}).catch(err => dispatch(groupDeleteStatusFail(groupId, statusId, err)));
};
const groupDeleteStatusRequest = (groupId: string, statusId: string) => ({
type: GROUP_DELETE_STATUS_REQUEST,
groupId,
statusId,
});
const groupDeleteStatusSuccess = (groupId: string, statusId: string) => ({
type: GROUP_DELETE_STATUS_SUCCESS,
groupId,
statusId,
});
const groupDeleteStatusFail = (groupId: string, statusId: string, error: AxiosError) => ({
type: GROUP_DELETE_STATUS_SUCCESS,
groupId,
statusId,
error,
});
const groupKick = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupKickRequest(groupId, accountId));
@ -677,9 +642,6 @@ export {
GROUP_RELATIONSHIPS_FETCH_REQUEST,
GROUP_RELATIONSHIPS_FETCH_SUCCESS,
GROUP_RELATIONSHIPS_FETCH_FAIL,
GROUP_DELETE_STATUS_REQUEST,
GROUP_DELETE_STATUS_SUCCESS,
GROUP_DELETE_STATUS_FAIL,
GROUP_KICK_REQUEST,
GROUP_KICK_SUCCESS,
GROUP_KICK_FAIL,
@ -735,10 +697,6 @@ export {
fetchGroupRelationshipsRequest,
fetchGroupRelationshipsSuccess,
fetchGroupRelationshipsFail,
groupDeleteStatus,
groupDeleteStatusRequest,
groupDeleteStatusSuccess,
groupDeleteStatusFail,
groupKick,
groupKickRequest,
groupKickSuccess,

View file

@ -0,0 +1,20 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useDeleteEntity } from 'soapbox/entity-store/hooks';
import { useApi } from 'soapbox/hooks';
import type { Group } from 'soapbox/schemas';
function useDeleteGroupStatus(group: Group, statusId: string) {
const api = useApi();
const { deleteEntity, isSubmitting } = useDeleteEntity(
Entities.STATUSES,
() => api.delete(`/api/v1/groups/${group.id}/statuses/${statusId}`),
);
return {
mutate: deleteEntity,
isSubmitting,
};
}
export { useDeleteGroupStatus };

View file

@ -94,7 +94,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
>
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
<span className='truncate'>{item.text}</span>
<span className='truncate font-medium'>{item.text}</span>
{item.count ? (
<span className='ml-auto h-5 w-5 flex-none'>

View file

@ -7,18 +7,20 @@ import { blockAccount } from 'soapbox/actions/accounts';
import { launchChat } from 'soapbox/actions/chats';
import { directCompose, mentionCompose, quoteCompose, replyCompose } from 'soapbox/actions/compose';
import { editEvent } from 'soapbox/actions/events';
import { groupBlock, groupDeleteStatus, groupKick } from 'soapbox/actions/groups';
import { toggleBookmark, toggleDislike, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions';
import { openModal } from 'soapbox/actions/modals';
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
import { initMuteModal } from 'soapbox/actions/mutes';
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
import { deleteFromTimelines } from 'soapbox/actions/timelines';
import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus';
import DropdownMenu from 'soapbox/components/dropdown-menu';
import StatusActionButton from 'soapbox/components/status-action-button';
import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper';
import { HStack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast';
import { isLocal, isRemote } from 'soapbox/utils/accounts';
import copy from 'soapbox/utils/copy';
@ -87,16 +89,7 @@ const messages = defineMessages({
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
replies_disabled_group: { id: 'status.disabled_replies.group_membership', defaultMessage: 'Only group members can reply' },
groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' },
groupModKick: { id: 'status.group_mod_kick', defaultMessage: 'Kick @{name} from group' },
groupModBlock: { id: 'status.group_mod_block', defaultMessage: 'Block @{name} from group' },
deleteFromGroupHeading: { id: 'confirmations.delete_from_group.heading', defaultMessage: 'Delete from group' },
deleteFromGroupMessage: { id: 'confirmations.delete_from_group.message', defaultMessage: 'Are you sure you want to delete @{name}\'s post?' },
kickFromGroupHeading: { id: 'confirmations.kick_from_group.heading', defaultMessage: 'Kick group member' },
kickFromGroupMessage: { id: 'confirmations.kick_from_group.message', defaultMessage: 'Are you sure you want to kick @{name} from this group?' },
kickFromGroupConfirm: { id: 'confirmations.kick_from_group.confirm', defaultMessage: 'Kick' },
blockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Block group member' },
blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to block @{name} from interacting with this group?' },
blockFromGroupConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' },
});
interface IStatusActionBar {
@ -121,6 +114,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const features = useFeatures();
const settings = useSettings();
const soapboxConfig = useSoapboxConfig();
const deleteGroupStatus = useDeleteGroupStatus(status?.group as Group, status.id);
const { allowedEmoji } = soapboxConfig;
@ -315,29 +309,13 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
heading: intl.formatMessage(messages.deleteHeading),
message: intl.formatMessage(messages.deleteFromGroupMessage, { name: <strong className='break-words'>{account.username}</strong> }),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(groupDeleteStatus((status.group as Group).id, status.id)),
}));
};
const handleKickFromGroup: React.EventHandler<React.MouseEvent> = () => {
const account = status.account as Account;
dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.kickFromGroupHeading),
message: intl.formatMessage(messages.kickFromGroupMessage, { name: <strong className='break-words'>{account.username}</strong> }),
confirm: intl.formatMessage(messages.kickFromGroupConfirm),
onConfirm: () => dispatch(groupKick((status.group as Group).id, account.id)),
}));
};
const handleBlockFromGroup: React.EventHandler<React.MouseEvent> = () => {
const account = status.account as Account;
dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.blockFromGroupHeading),
message: intl.formatMessage(messages.blockFromGroupMessage, { name: <strong className='break-words'>{account.username}</strong> }),
confirm: intl.formatMessage(messages.blockFromGroupConfirm),
onConfirm: () => dispatch(groupBlock((status.group as Group).id, account.id)),
onConfirm: () => {
deleteGroupStatus.mutate(status.id, {
onSuccess() {
dispatch(deleteFromTimelines(status.id));
},
});
},
}));
};
@ -362,7 +340,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
menu.push({
text: intl.formatMessage(messages.copy),
action: handleCopy,
icon: require('@tabler/icons/link.svg'),
icon: require('@tabler/icons/clipboard-copy.svg'),
});
if (features.embeds && isLocal(account)) {
@ -466,7 +444,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
menu.push({
text: intl.formatMessage(messages.mute, { name: username }),
action: handleMuteClick,
icon: require('@tabler/icons/circle-x.svg'),
icon: require('@tabler/icons/volume-3.svg'),
});
menu.push({
text: intl.formatMessage(messages.block, { name: username }),
@ -480,23 +458,17 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
});
}
if (status.group && groupRelationship?.role && ['admin', 'moderator'].includes(groupRelationship.role)) {
if (status.group &&
groupRelationship?.role &&
[GroupRoles.OWNER].includes(groupRelationship.role) &&
!ownAccount
) {
menu.push(null);
menu.push({
text: intl.formatMessage(messages.groupModDelete),
action: handleDeleteFromGroup,
icon: require('@tabler/icons/trash.svg'),
});
// TODO: figure out when an account is not in the group anymore
menu.push({
text: intl.formatMessage(messages.groupModKick, { name: account.get('username') }),
action: handleKickFromGroup,
icon: require('@tabler/icons/user-minus.svg'),
});
menu.push({
text: intl.formatMessage(messages.groupModBlock, { name: account.get('username') }),
action: handleBlockFromGroup,
icon: require('@tabler/icons/ban.svg'),
destructive: true,
});
}

View file

@ -1440,7 +1440,7 @@
"status.cancel_reblog_private": "Un-repost",
"status.cannot_reblog": "This post cannot be reposted",
"status.chat": "Chat with @{name}",
"status.copy": "Copy link to post",
"status.copy": "Copy Link to Post",
"status.delete": "Delete",
"status.detailed_status": "Detailed conversation view",
"status.direct": "Direct message @{name}",
@ -1462,8 +1462,8 @@
"status.load_more": "Load more",
"status.mention": "Mention @{name}",
"status.more": "More",
"status.mute_conversation": "Mute conversation",
"status.open": "Expand this post",
"status.mute_conversation": "Mute Conversation",
"status.open": "Show Post Details",
"status.pin": "Pin on profile",
"status.pinned": "Pinned post",
"status.quote": "Quote post",
@ -1499,7 +1499,7 @@
"status.translated_from_with": "Translated from {lang} using {provider}",
"status.unbookmark": "Remove bookmark",
"status.unbookmarked": "Bookmark removed.",
"status.unmute_conversation": "Unmute conversation",
"status.unmute_conversation": "Unmute Conversation",
"status.unpin": "Unpin from profile",
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
"statuses.quote_tombstone": "Post is unavailable.",