Cap Group Admins at 5

This commit is contained in:
Chewbacca 2023-04-04 14:05:08 -04:00
parent b704e476eb
commit 5fa8a21403
6 changed files with 65 additions and 27 deletions

View file

@ -8,6 +8,8 @@ import { ToastText, ToastType } from 'soapbox/toast';
import HStack from '../hstack/hstack'; import HStack from '../hstack/hstack';
import Icon from '../icon/icon'; import Icon from '../icon/icon';
import Stack from '../stack/stack';
import Text from '../text/text';
const renderText = (text: ToastText) => { const renderText = (text: ToastText) => {
if (typeof text === 'string') { if (typeof text === 'string') {
@ -24,13 +26,14 @@ interface IToast {
action?(): void action?(): void
actionLink?: string actionLink?: string
actionLabel?: ToastText actionLabel?: ToastText
summary?: string
} }
/** /**
* Customizable Toasts for in-app notifications. * Customizable Toasts for in-app notifications.
*/ */
const Toast = (props: IToast) => { const Toast = (props: IToast) => {
const { t, message, type, action, actionLink, actionLabel } = props; const { t, message, type, action, actionLink, actionLabel, summary } = props;
const dismissToast = () => toast.dismiss(t.id); const dismissToast = () => toast.dismiss(t.id);
@ -109,35 +112,41 @@ const Toast = (props: IToast) => {
}) })
} }
> >
<HStack space={4} alignItems='start'> <Stack space={2}>
<HStack space={3} justifyContent='between' alignItems='start' className='w-0 flex-1'> <HStack space={4} alignItems='start'>
<HStack space={3} alignItems='start' className='w-0 flex-1'> <HStack space={3} justifyContent='between' alignItems='start' className='w-0 flex-1'>
<div className='shrink-0'> <HStack space={3} alignItems='start' className='w-0 flex-1'>
{renderIcon()} <div className='shrink-0'>
</div> {renderIcon()}
</div>
<p className='pt-0.5 text-sm text-gray-900 dark:text-gray-100' data-testid='toast-message'> <Text size='sm' data-testid='toast-message' className='pt-0.5'>
{renderText(message)} {renderText(message)}
</p> </Text>
</HStack>
{/* Action */}
{renderAction()}
</HStack> </HStack>
{/* Action */} {/* Dismiss Button */}
{renderAction()} <div className='flex shrink-0 pt-0.5'>
<button
type='button'
className='inline-flex rounded-md text-gray-600 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:text-gray-600 dark:hover:text-gray-500'
onClick={dismissToast}
data-testid='toast-dismiss'
>
<span className='sr-only'>Close</span>
<Icon src={require('@tabler/icons/x.svg')} className='h-5 w-5' />
</button>
</div>
</HStack> </HStack>
{/* Dismiss Button */} {summary ? (
<div className='flex shrink-0 pt-0.5'> <Text theme='muted' size='sm'>{summary}</Text>
<button ) : null}
type='button' </Stack>
className='inline-flex rounded-md text-gray-600 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:text-gray-600 dark:hover:text-gray-500'
onClick={dismissToast}
data-testid='toast-dismiss'
>
<span className='sr-only'>Close</span>
<Icon src={require('@tabler/icons/x.svg')} className='h-5 w-5' />
</button>
</div>
</HStack>
</div> </div>
); );
}; };

View file

@ -15,10 +15,14 @@ import { useAccount, useBlockGroupMember, useDemoteGroupMember, usePromoteGroupM
import { GroupRoles } from 'soapbox/schemas/group-member'; import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
import { MAX_ADMIN_COUNT } from '../group-members';
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu'; import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
import type { Group, GroupMember } from 'soapbox/types/entities'; import type { Group, GroupMember } from 'soapbox/types/entities';
const messages = defineMessages({ const messages = defineMessages({
adminLimitTitle: { id: 'group.member.admin.limit.title', defaultMessage: 'Admin limit reached' },
adminLimitSummary: { id: 'group.member.admin.limit.summary', defaultMessage: 'You can assign up to {count} admins for the group at this time.' },
blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban' }, blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban' },
blockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Ban From Group' }, blockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Ban From Group' },
blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to ban @{name} from the group?' }, blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to ban @{name} from the group?' },
@ -39,10 +43,11 @@ const messages = defineMessages({
interface IGroupMemberListItem { interface IGroupMemberListItem {
member: GroupMember member: GroupMember
group: Group group: Group
canPromoteToAdmin: boolean
} }
const GroupMemberListItem = (props: IGroupMemberListItem) => { const GroupMemberListItem = (props: IGroupMemberListItem) => {
const { member, group } = props; const { canPromoteToAdmin, member, group } = props;
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const features = useFeatures(); const features = useFeatures();
@ -90,6 +95,13 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
}; };
const handleAdminAssignment = () => { const handleAdminAssignment = () => {
if (!canPromoteToAdmin) {
toast.error(intl.formatMessage(messages.adminLimitTitle), {
summary: intl.formatMessage(messages.adminLimitSummary, { count: MAX_ADMIN_COUNT }),
});
return;
}
dispatch(openModal('CONFIRM', { dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.promoteConfirm), heading: intl.formatMessage(messages.promoteConfirm),
message: intl.formatMessage(messages.promoteConfirmMessage, { name: account?.username }), message: intl.formatMessage(messages.promoteConfirmMessage, { name: account?.username }),

View file

@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { PendingItemsRow } from 'soapbox/components/pending-items-row'; import { PendingItemsRow } from 'soapbox/components/pending-items-row';
import ScrollableList from 'soapbox/components/scrollable-list'; import ScrollableList from 'soapbox/components/scrollable-list';
import { useFeatures } from 'soapbox/hooks';
import { useGroup } from 'soapbox/hooks/api'; import { useGroup } from 'soapbox/hooks/api';
import { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests'; import { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests';
import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers'; import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers';
@ -18,9 +19,13 @@ interface IGroupMembers {
params: { id: string } params: { id: string }
} }
export const MAX_ADMIN_COUNT = 5;
const GroupMembers: React.FC<IGroupMembers> = (props) => { const GroupMembers: React.FC<IGroupMembers> = (props) => {
const groupId = props.params.id; const groupId = props.params.id;
const features = useFeatures();
const { group, isFetching: isFetchingGroup } = useGroup(groupId); const { group, isFetching: isFetchingGroup } = useGroup(groupId);
const { groupMembers: owners, isFetching: isFetchingOwners } = useGroupMembers(groupId, GroupRoles.OWNER); const { groupMembers: owners, isFetching: isFetchingOwners } = useGroupMembers(groupId, GroupRoles.OWNER);
const { groupMembers: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, GroupRoles.ADMIN); const { groupMembers: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, GroupRoles.ADMIN);
@ -35,6 +40,10 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
...users, ...users,
], [owners, admins, users]); ], [owners, admins, users]);
const canPromoteToAdmin = features.groupsAdminMax
? members.filter((member) => member.role === GroupRoles.ADMIN).length < MAX_ADMIN_COUNT
: true;
return ( return (
<> <>
<ScrollableList <ScrollableList
@ -58,6 +67,7 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
group={group as Group} group={group as Group}
member={member} member={member}
key={member.account.id} key={member.account.id}
canPromoteToAdmin={canPromoteToAdmin}
/> />
))} ))}
</ScrollableList> </ScrollableList>

View file

@ -1,10 +1,11 @@
import { Entities } from 'soapbox/entity-store/entities'; import { Entities } from 'soapbox/entity-store/entities';
import { useEntities } from 'soapbox/entity-store/hooks'; import { useEntities } from 'soapbox/entity-store/hooks';
import { GroupMember, groupMemberSchema } from 'soapbox/schemas'; import { GroupMember, groupMemberSchema } from 'soapbox/schemas';
import { GroupRoles } from 'soapbox/schemas/group-member';
import { useApi } from '../useApi'; import { useApi } from '../useApi';
function useGroupMembers(groupId: string, role: string) { function useGroupMembers(groupId: string, role: GroupRoles) {
const api = useApi(); const api = useApi();
const { entities, ...result } = useEntities<GroupMember>( const { entities, ...result } = useEntities<GroupMember>(

View file

@ -14,6 +14,7 @@ interface IToastOptions {
actionLink?: string actionLink?: string
actionLabel?: ToastText actionLabel?: ToastText
duration?: number duration?: number
summary?: string
} }
const DEFAULT_DURATION = 4000; const DEFAULT_DURATION = 4000;

View file

@ -529,6 +529,11 @@ const getInstanceFeatures = (instance: Instance) => {
*/ */
groups: v.build === UNRELEASED, groups: v.build === UNRELEASED,
/**
* Cap # of Group Admins to 5
*/
groupsAdminMax: v.software === TRUTHSOCIAL,
/** /**
* Can see trending/suggested Groups. * Can see trending/suggested Groups.
*/ */