diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 8872af678..3f18f5e70 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -8,6 +8,7 @@ import { showAlertForError } from 'soapbox/actions/alerts'; 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, toggleFavourite, togglePin, toggleReblog } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation'; @@ -24,7 +25,7 @@ import copy from 'soapbox/utils/copy'; import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji-reacts'; import type { Menu } from 'soapbox/components/dropdown-menu'; -import type { Account, Status } from 'soapbox/types/entities'; +import type { Account, Group, Status } from 'soapbox/types/entities'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -81,6 +82,18 @@ const messages = defineMessages({ redraftHeading: { id: 'confirmations.redraft.heading', defaultMessage: 'Delete & redraft' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, 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 { @@ -103,6 +116,7 @@ const StatusActionBar: React.FC = ({ const dispatch = useAppDispatch(); const me = useAppSelector(state => state.me); + const groupRelationship = useAppSelector(state => status.group ? state.group_relationships.get((status.group as Group).id) : null); const features = useFeatures(); const settings = useSettings(); const soapboxConfig = useSoapboxConfig(); @@ -285,6 +299,39 @@ const StatusActionBar: React.FC = ({ dispatch(toggleStatusSensitivityModal(intl, status.id, status.sensitive)); }; + const handleDeleteFromGroup: React.EventHandler = () => { + const account = status.account as Account; + + dispatch(openModal('CONFIRM', { + 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)), + })); + }; + const _makeMenu = (publicStatus: boolean) => { const mutingConversation = status.muted; const ownAccount = status.getIn(['account', 'id']) === me; @@ -425,6 +472,26 @@ const StatusActionBar: React.FC = ({ }); } + if (status.group && groupRelationship?.role && ['admin', 'moderator'].includes(groupRelationship.role)) { + 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'), + }); + } + if (isStaff) { menu.push(null); @@ -491,6 +558,7 @@ const StatusActionBar: React.FC = ({ const menu = _makeMenu(publicStatus); let reblogIcon = require('@tabler/icons/repeat.svg'); let replyTitle; + let replyDisabled = false; if (status.visibility === 'direct') { reblogIcon = require('@tabler/icons/mail.svg'); @@ -498,6 +566,11 @@ const StatusActionBar: React.FC = ({ reblogIcon = require('@tabler/icons/lock.svg'); } + if ((status.group as Group)?.membership_required && !groupRelationship?.member) { + replyDisabled = true; + replyTitle = intl.formatMessage(messages.replies_disabled_group); + } + const reblogMenu = [{ text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), action: handleReblogClick, @@ -543,6 +616,7 @@ const StatusActionBar: React.FC = ({ onClick={handleReplyClick} count={replyCount} text={withLabels ? intl.formatMessage(messages.reply) : undefined} + disabled={replyDisabled} /> {(features.quotePosts && me) ? ( diff --git a/app/soapbox/features/ui/components/who-to-follow-panel.tsx b/app/soapbox/features/ui/components/who-to-follow-panel.tsx index dd2fc91fa..1573f2573 100644 --- a/app/soapbox/features/ui/components/who-to-follow-panel.tsx +++ b/app/soapbox/features/ui/components/who-to-follow-panel.tsx @@ -38,7 +38,9 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => { title={} action={ - View all + + + } >