diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index 690b74540..ad760d916 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -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, diff --git a/app/soapbox/api/hooks/groups/useDeleteGroupStatus.ts b/app/soapbox/api/hooks/groups/useDeleteGroupStatus.ts new file mode 100644 index 000000000..55a6f9459 --- /dev/null +++ b/app/soapbox/api/hooks/groups/useDeleteGroupStatus.ts @@ -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 }; \ No newline at end of file diff --git a/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx b/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx index 8b0ca7755..8a6c8f531 100644 --- a/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx +++ b/app/soapbox/components/dropdown-menu/dropdown-menu-item.tsx @@ -94,7 +94,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => { > {item.icon && } - {item.text} + {item.text} {item.count ? ( diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 0f4adb552..13e1a4a3c 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -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 = ({ 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 = ({ heading: intl.formatMessage(messages.deleteHeading), message: intl.formatMessage(messages.deleteFromGroupMessage, { name: {account.username} }), confirm: intl.formatMessage(messages.deleteConfirm), - onConfirm: () => dispatch(groupDeleteStatus((status.group as Group).id, status.id)), - })); - }; - - const handleKickFromGroup: React.EventHandler = () => { - const account = status.account as Account; - - dispatch(openModal('CONFIRM', { - heading: intl.formatMessage(messages.kickFromGroupHeading), - message: intl.formatMessage(messages.kickFromGroupMessage, { name: {account.username} }), - confirm: intl.formatMessage(messages.kickFromGroupConfirm), - onConfirm: () => dispatch(groupKick((status.group as Group).id, account.id)), - })); - }; - - const handleBlockFromGroup: React.EventHandler = () => { - const account = status.account as Account; - - dispatch(openModal('CONFIRM', { - heading: intl.formatMessage(messages.blockFromGroupHeading), - message: intl.formatMessage(messages.blockFromGroupMessage, { name: {account.username} }), - 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 = ({ 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 = ({ 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 = ({ }); } - 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, }); } diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 91fad97e0..a07a6457d 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -488,7 +488,6 @@ "confirmations.delete_event.confirm": "Delete", "confirmations.delete_event.heading": "Delete event", "confirmations.delete_event.message": "Are you sure you want to delete this event?", - "confirmations.delete_from_group.heading": "Delete from group", "confirmations.delete_from_group.message": "Are you sure you want to delete @{name}'s post?", "confirmations.delete_group.confirm": "Delete", "confirmations.delete_group.heading": "Delete Group", @@ -500,7 +499,6 @@ "confirmations.domain_block.heading": "Block {domain}", "confirmations.domain_block.message": "Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications.", "confirmations.kick_from_group.confirm": "Kick", - "confirmations.kick_from_group.heading": "Kick group member", "confirmations.kick_from_group.message": "Are you sure you want to kick @{name} from this group?", "confirmations.leave_event.confirm": "Leave event", "confirmations.leave_event.message": "If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?", @@ -1440,7 +1438,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}", @@ -1452,9 +1450,7 @@ "status.favourite": "Like", "status.filtered": "Filtered", "status.group": "Posted in {group}", - "status.group_mod_block": "Block @{name} from group", "status.group_mod_delete": "Delete post from group", - "status.group_mod_kick": "Kick @{name} from group", "status.interactions.dislikes": "{count, plural, one {Dislike} other {Dislikes}}", "status.interactions.favourites": "{count, plural, one {Like} other {Likes}}", "status.interactions.quotes": "{count, plural, one {Quote} other {Quotes}}", @@ -1462,8 +1458,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 +1495,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.",