import debounce from 'lodash/debounce'; import React, { useCallback, useEffect } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { expandGroupMemberships, fetchGroup, fetchGroupMemberships, groupBlock, groupDemoteAccount, groupKick, groupPromoteAccount } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; import Account from 'soapbox/components/account'; import ScrollableList from 'soapbox/components/scrollable-list'; import { CardHeader, CardTitle, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui'; import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; import toast from 'soapbox/toast'; import PlaceholderAccount from '../placeholder/components/placeholder-account'; import type { Menu as MenuType } from 'soapbox/components/dropdown-menu'; import type { GroupRole, List } from 'soapbox/reducers/group-memberships'; import type { GroupRelationship } from 'soapbox/types/entities'; type RouteParams = { id: string }; const messages = defineMessages({ adminSubheading: { id: 'group.admin_subheading', defaultMessage: 'Group administrators' }, moderatorSubheading: { id: 'group.moderator_subheading', defaultMessage: 'Group moderators' }, userSubheading: { id: 'group.user_subheading', defaultMessage: 'Users' }, groupModKick: { id: 'group.group_mod_kick', defaultMessage: 'Kick @{name} from group' }, groupModBlock: { id: 'group.group_mod_block', defaultMessage: 'Block @{name} from group' }, groupModPromoteAdmin: { id: 'group.group_mod_promote_admin', defaultMessage: 'Promote @{name} to group administrator' }, groupModPromoteMod: { id: 'group.group_mod_promote_mod', defaultMessage: 'Promote @{name} to group moderator' }, groupModDemote: { id: 'group.group_mod_demote', defaultMessage: 'Demote @{name}' }, kickFromGroupMessage: { id: 'confirmations.kick_from_group.message', defaultMessage: 'Are you sure you want to kick @{name} from this group?' }, kickConfirm: { id: 'confirmations.kick_from_group.confirm', defaultMessage: 'Kick' }, blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to block @{name} from interacting with this group?' }, blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' }, promoteConfirmMessage: { id: 'confirmations.promote_in_group.message', defaultMessage: 'Are you sure you want to promote @{name}? You will not be able to demote them.' }, promoteConfirm: { id: 'confirmations.promote_in_group.confirm', defaultMessage: 'Promote' }, kicked: { id: 'group.group_mod_kick.success', defaultMessage: 'Kicked @{name} from group' }, blocked: { id: 'group.group_mod_block.success', defaultMessage: 'Blocked @{name} from group' }, promotedToAdmin: { id: 'group.group_mod_promote_admin.success', defaultMessage: 'Promoted @{name} to group administrator' }, promotedToMod: { id: 'group.group_mod_promote_mod.success', defaultMessage: 'Promoted @{name} to group moderator' }, demotedToUser: { id: 'group.group_mod_demote.success', defaultMessage: 'Demoted @{name} to group user' }, }); interface IGroupMember { accountId: string accountRole: GroupRole groupId: string relationship?: GroupRelationship } const GroupMember: React.FC = ({ accountId, accountRole, groupId, relationship }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const getAccount = useCallback(makeGetAccount(), []); const account = useAppSelector((state) => getAccount(state, accountId)); if (!account) return null; const handleKickFromGroup = () => { dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.kickFromGroupMessage, { name: account.username }), confirm: intl.formatMessage(messages.kickConfirm), onConfirm: () => dispatch(groupKick(groupId, account.id)).then(() => toast.success(intl.formatMessage(messages.kicked, { name: account.acct })), ), })); }; const handleBlockFromGroup = () => { dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.blockFromGroupMessage, { name: account.username }), confirm: intl.formatMessage(messages.blockConfirm), onConfirm: () => dispatch(groupBlock(groupId, account.id)).then(() => toast.success(intl.formatMessage(messages.blocked, { name: account.acct })), ), })); }; const onPromote = (role: 'admin' | 'moderator', warning?: boolean) => { if (warning) { return dispatch(openModal('CONFIRM', { message: intl.formatMessage(messages.promoteConfirmMessage, { name: account.username }), confirm: intl.formatMessage(messages.promoteConfirm), onConfirm: () => dispatch(groupPromoteAccount(groupId, account.id, role)).then(() => toast.success(intl.formatMessage(role === 'admin' ? messages.promotedToAdmin : messages.promotedToMod, { name: account.acct })), ), })); } else { return dispatch(groupPromoteAccount(groupId, account.id, role)).then(() => toast.success(intl.formatMessage(role === 'admin' ? messages.promotedToAdmin : messages.promotedToMod, { name: account.acct })), ); } }; const handlePromoteToGroupAdmin = () => { onPromote('admin', true); }; const handlePromoteToGroupMod = () => { onPromote('moderator', relationship!.role === 'moderator'); }; const handleDemote = () => { dispatch(groupDemoteAccount(groupId, account.id, 'user')).then(() => toast.success(intl.formatMessage(messages.demotedToUser, { name: account.acct })), ).catch(() => {}); }; const makeMenu = () => { const menu: MenuType = []; if (!relationship || !relationship.role) return menu; if (['admin', 'moderator'].includes(relationship.role) && ['moderator', 'user'].includes(accountRole) && accountRole !== relationship.role) { menu.push({ text: intl.formatMessage(messages.groupModKick, { name: account.username }), icon: require('@tabler/icons/user-minus.svg'), action: handleKickFromGroup, }); menu.push({ text: intl.formatMessage(messages.groupModBlock, { name: account.username }), icon: require('@tabler/icons/ban.svg'), action: handleBlockFromGroup, }); } if (relationship.role === 'admin' && accountRole !== 'admin' && account.acct === account.username) { menu.push(null); switch (accountRole) { case 'moderator': menu.push({ text: intl.formatMessage(messages.groupModPromoteAdmin, { name: account.username }), icon: require('@tabler/icons/arrow-up-circle.svg'), action: handlePromoteToGroupAdmin, }); menu.push({ text: intl.formatMessage(messages.groupModDemote, { name: account.username }), icon: require('@tabler/icons/arrow-down-circle.svg'), action: handleDemote, }); break; case 'user': menu.push({ text: intl.formatMessage(messages.groupModPromoteMod, { name: account.username }), icon: require('@tabler/icons/arrow-up-circle.svg'), action: handlePromoteToGroupMod, }); break; } } return menu; }; const menu = makeMenu(); return (
{menu.length > 0 && ( {menu.map((menuItem, idx) => { if (typeof menuItem?.text === 'undefined') { return ; } else { const Comp = (menuItem.action ? MenuItem : MenuLink) as any; const itemProps = menuItem.action ? { onSelect: menuItem.action } : { to: menuItem.to, as: Link, target: menuItem.newTab ? '_blank' : '_self' }; return ( {menuItem.icon && ( )}
{menuItem.text}
); } })}
)}
); }; interface IGroupMembers { params: RouteParams } const GroupMembers: React.FC = (props) => { const intl = useIntl(); const dispatch = useAppDispatch(); const groupId = props.params.id; const relationship = useAppSelector((state) => state.group_relationships.get(groupId)); const admins = useAppSelector((state) => state.group_memberships.admin.get(groupId)); const moderators = useAppSelector((state) => state.group_memberships.moderator.get(groupId)); const users = useAppSelector((state) => state.group_memberships.user.get(groupId)); const handleLoadMore = (role: 'admin' | 'moderator' | 'user') => { dispatch(expandGroupMemberships(groupId, role)); }; const handleLoadMoreAdmins = useCallback(debounce(() => { handleLoadMore('admin'); }, 300, { leading: true }), []); const handleLoadMoreModerators = useCallback(debounce(() => { handleLoadMore('moderator'); }, 300, { leading: true }), []); const handleLoadMoreUsers = useCallback(debounce(() => { handleLoadMore('user'); }, 300, { leading: true }), []); const renderMemberships = (memberships: List | undefined, role: GroupRole, handler: () => void) => { if (!memberships?.isLoading && !memberships?.items.count()) return; return ( {memberships?.items?.map(accountId => ( ))} ); }; useEffect(() => { dispatch(fetchGroup(groupId)); dispatch(fetchGroupMemberships(groupId, 'admin')); dispatch(fetchGroupMemberships(groupId, 'moderator')); dispatch(fetchGroupMemberships(groupId, 'user')); }, [groupId]); return ( <> {renderMemberships(admins, 'admin', handleLoadMoreAdmins)} {renderMemberships(moderators, 'moderator', handleLoadMoreModerators)} {renderMemberships(users, 'user', handleLoadMoreUsers)} ); }; export default GroupMembers;