diff --git a/app/soapbox/components/ui/toast/toast.tsx b/app/soapbox/components/ui/toast/toast.tsx index 38bda317e..ce6847ced 100644 --- a/app/soapbox/components/ui/toast/toast.tsx +++ b/app/soapbox/components/ui/toast/toast.tsx @@ -8,6 +8,8 @@ import { ToastText, ToastType } from 'soapbox/toast'; import HStack from '../hstack/hstack'; import Icon from '../icon/icon'; +import Stack from '../stack/stack'; +import Text from '../text/text'; const renderText = (text: ToastText) => { if (typeof text === 'string') { @@ -24,13 +26,14 @@ interface IToast { action?(): void actionLink?: string actionLabel?: ToastText + summary?: string } /** * Customizable Toasts for in-app notifications. */ 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); @@ -109,35 +112,46 @@ const Toast = (props: IToast) => { }) } > - - - -
- {renderIcon()} -
- -

- {renderText(message)} -

+ + + + +
+ {renderIcon()} +
+ + + {renderText(message)} + +
+ + {/* Action */} + {renderAction()}
- {/* Action */} - {renderAction()} + {/* Dismiss Button */} +
+ +
- {/* Dismiss Button */} -
- -
-
+ {summary ? ( + {summary} + ) : null} + ); }; diff --git a/app/soapbox/features/group/components/group-member-list-item.tsx b/app/soapbox/features/group/components/group-member-list-item.tsx index 4fa3ccb7a..c88f4e58d 100644 --- a/app/soapbox/features/group/components/group-member-list-item.tsx +++ b/app/soapbox/features/group/components/group-member-list-item.tsx @@ -15,10 +15,14 @@ import { useAccount, useBlockGroupMember, useDemoteGroupMember, usePromoteGroupM import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; +import { MAX_ADMIN_COUNT } from '../group-members'; + import type { Menu as IMenu } from 'soapbox/components/dropdown-menu'; import type { Group, GroupMember } from 'soapbox/types/entities'; 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' }, 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?' }, @@ -39,10 +43,11 @@ const messages = defineMessages({ interface IGroupMemberListItem { member: GroupMember group: Group + canPromoteToAdmin: boolean } const GroupMemberListItem = (props: IGroupMemberListItem) => { - const { member, group } = props; + const { canPromoteToAdmin, member, group } = props; const dispatch = useAppDispatch(); const features = useFeatures(); @@ -90,6 +95,13 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { }; const handleAdminAssignment = () => { + if (!canPromoteToAdmin) { + toast.error(intl.formatMessage(messages.adminLimitTitle), { + summary: intl.formatMessage(messages.adminLimitSummary, { count: MAX_ADMIN_COUNT }), + }); + return; + } + dispatch(openModal('CONFIRM', { heading: intl.formatMessage(messages.promoteConfirm), message: intl.formatMessage(messages.promoteConfirmMessage, { name: account?.username }), diff --git a/app/soapbox/features/group/group-members.tsx b/app/soapbox/features/group/group-members.tsx index 2facf912f..a3a790a62 100644 --- a/app/soapbox/features/group/group-members.tsx +++ b/app/soapbox/features/group/group-members.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from 'react'; import { PendingItemsRow } from 'soapbox/components/pending-items-row'; import ScrollableList from 'soapbox/components/scrollable-list'; +import { useFeatures } from 'soapbox/hooks'; import { useGroup } from 'soapbox/hooks/api'; import { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests'; import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers'; @@ -18,9 +19,13 @@ interface IGroupMembers { params: { id: string } } +export const MAX_ADMIN_COUNT = 5; + const GroupMembers: React.FC = (props) => { const groupId = props.params.id; + const features = useFeatures(); + const { group, isFetching: isFetchingGroup } = useGroup(groupId); const { groupMembers: owners, isFetching: isFetchingOwners } = useGroupMembers(groupId, GroupRoles.OWNER); const { groupMembers: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, GroupRoles.ADMIN); @@ -35,6 +40,10 @@ const GroupMembers: React.FC = (props) => { ...users, ], [owners, admins, users]); + const canPromoteToAdmin = features.groupsAdminMax + ? members.filter((member) => member.role === GroupRoles.ADMIN).length < MAX_ADMIN_COUNT + : true; + return ( <> = (props) => { group={group as Group} member={member} key={member.account.id} + canPromoteToAdmin={canPromoteToAdmin} /> ))} diff --git a/app/soapbox/hooks/api/useGroupMembers.ts b/app/soapbox/hooks/api/useGroupMembers.ts index 669f1c082..3a1c99af5 100644 --- a/app/soapbox/hooks/api/useGroupMembers.ts +++ b/app/soapbox/hooks/api/useGroupMembers.ts @@ -1,10 +1,11 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; import { GroupMember, groupMemberSchema } from 'soapbox/schemas'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import { useApi } from '../useApi'; -function useGroupMembers(groupId: string, role: string) { +function useGroupMembers(groupId: string, role: GroupRoles) { const api = useApi(); const { entities, ...result } = useEntities( diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index af5a058c6..145b26e49 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -789,6 +789,8 @@ "group.leave.label": "Leave", "group.leave.success": "Left the group", "group.manage": "Manage Group", + "group.member.admin.limit.summary": "You can assign up to {count} admins for the group at this time.", + "group.member.admin.limit.title": "Admin limit reached", "group.popover.action": "View Group", "group.popover.summary": "You must be a member of the group in order to reply to this status.", "group.popover.title": "Membership required", diff --git a/app/soapbox/toast.tsx b/app/soapbox/toast.tsx index 6095737f4..820b68b86 100644 --- a/app/soapbox/toast.tsx +++ b/app/soapbox/toast.tsx @@ -14,6 +14,7 @@ interface IToastOptions { actionLink?: string actionLabel?: ToastText duration?: number + summary?: string } const DEFAULT_DURATION = 4000; diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 8558e3b9d..afbece3b4 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -529,6 +529,11 @@ const getInstanceFeatures = (instance: Instance) => { */ groups: v.build === UNRELEASED, + /** + * Cap # of Group Admins to 5 + */ + groupsAdminMax: v.software === TRUTHSOCIAL, + /** * Can see trending/suggested Groups. */