diff --git a/app/soapbox/components/groups/group-avatar.tsx b/app/soapbox/components/groups/group-avatar.tsx index b862a92c0..91d6808d2 100644 --- a/app/soapbox/components/groups/group-avatar.tsx +++ b/app/soapbox/components/groups/group-avatar.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import React from 'react'; -import { useGroupRoles } from 'soapbox/hooks/useGroupRoles'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import { Avatar } from '../ui'; @@ -16,17 +16,15 @@ interface IGroupAvatar { const GroupAvatar = (props: IGroupAvatar) => { const { group, size, withRing = false } = props; - const { normalizeRole } = useGroupRoles(); - - const isAdmin = normalizeRole(group.relationship?.role as any) === 'admin'; + const isOwner = group.relationship?.role === GroupRoles.OWNER; return ( { + onSuccess?(entity: TEntity): void +} + function useEntityActions( path: EntityPath, endpoints: EntityActionEndpoints, @@ -38,7 +42,7 @@ function useEntityActions( const getState = useGetState(); const [entityType, listKey] = path; - function createEntity(params: P): Promise> { + function createEntity(params: P, callbacks: EntityCallbacks = {}): Promise> { if (!endpoints.post) return Promise.reject(endpoints); return api.post(endpoints.post, params).then((response) => { @@ -48,6 +52,10 @@ function useEntityActions( // TODO: optimistic updating dispatch(importEntities([entity], entityType, listKey)); + if (callbacks.onSuccess) { + callbacks.onSuccess(entity); + } + return { response, entity, 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 af701c7f8..10ad4e0cb 100644 --- a/app/soapbox/features/group/components/group-member-list-item.tsx +++ b/app/soapbox/features/group/components/group-member-list-item.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import React, { useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { groupDemoteAccount, groupKick, groupPromoteAccount } from 'soapbox/actions/groups'; +import { groupKick } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; import Account from 'soapbox/components/account'; import DropdownMenu from 'soapbox/components/dropdown-menu/dropdown-menu'; @@ -10,31 +10,29 @@ import { HStack } from 'soapbox/components/ui'; import { deleteEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; import { useAccount, useAppDispatch, useFeatures } from 'soapbox/hooks'; -import { useBlockGroupMember } from 'soapbox/hooks/api/groups/useBlockGroupMember'; -import { BaseGroupRoles, useGroupRoles } from 'soapbox/hooks/useGroupRoles'; +import { useBlockGroupMember, useDemoteGroupMember, usePromoteGroupMember } from 'soapbox/hooks/api'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; import type { Menu as IMenu } from 'soapbox/components/dropdown-menu'; import type { Account as AccountEntity, Group, GroupMember } from 'soapbox/types/entities'; const messages = defineMessages({ - blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' }, + 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?' }, - blocked: { id: 'group.group_mod_block.success', defaultMessage: 'You have successfully blocked @{name} from the group' }, - demotedToUser: { id: 'group.group_mod_demote.success', defaultMessage: 'Demoted @{name} to group user' }, + blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' }, + demotedToUser: { id: 'group.demote.user.success', defaultMessage: '@{name} is now a member' }, groupModBlock: { id: 'group.group_mod_block', defaultMessage: 'Ban from group' }, - groupModDemote: { id: 'group.group_mod_demote', defaultMessage: 'Demote @{name}' }, + groupModDemote: { id: 'group.group_mod_demote', defaultMessage: 'Remove {role} role' }, groupModKick: { id: 'group.group_mod_kick', defaultMessage: 'Kick @{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' }, + groupModPromoteMod: { id: 'group.group_mod_promote_mod', defaultMessage: 'Assign {role} role' }, kickConfirm: { id: 'confirmations.kick_from_group.confirm', defaultMessage: 'Kick' }, kickFromGroupMessage: { id: 'confirmations.kick_from_group.message', defaultMessage: 'Are you sure you want to kick @{name} from this group?' }, kicked: { id: 'group.group_mod_kick.success', defaultMessage: 'Kicked @{name} from group' }, - promoteConfirm: { id: 'confirmations.promote_in_group.confirm', defaultMessage: 'Promote' }, - promoteConfirmMessage: { id: 'confirmations.promote_in_group.message', defaultMessage: 'Are you sure you want to promote @{name}? You will not be able to demote them.' }, - 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' }, + promoteConfirm: { id: 'group.promote.admin.confirmation.title', defaultMessage: 'Assign Admin Role' }, + promoteConfirmMessage: { id: 'group.promote.admin.confirmation.message', defaultMessage: 'Are you sure you want to assign the admin role to @{name}?' }, + promotedToAdmin: { id: 'group.promote.admin.success', defaultMessage: '@{name} is now an admin' }, }); interface IGroupMemberListItem { @@ -49,19 +47,20 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { const features = useFeatures(); const intl = useIntl(); - const { normalizeRole } = useGroupRoles(); const blockGroupMember = useBlockGroupMember(group, member); + const promoteGroupMember = usePromoteGroupMember(group, member); + const demoteGroupMember = useDemoteGroupMember(group, member); const account = useAccount(member.account.id) as AccountEntity; // Current user role - const isCurrentUserAdmin = normalizeRole(group.relationship?.role as any) === BaseGroupRoles.ADMIN; - const isCurrentUserModerator = normalizeRole(group.relationship?.role as any) === BaseGroupRoles.MODERATOR; + const isCurrentUserOwner = group.relationship?.role === GroupRoles.OWNER; + const isCurrentUserAdmin = group.relationship?.role === GroupRoles.ADMIN; // Member role - const isMemberAdmin = normalizeRole(member.role as any) === BaseGroupRoles.ADMIN; - const isMemberModerator = normalizeRole(member.role as any) === BaseGroupRoles.MODERATOR; - const isMemberUser = normalizeRole(member.role as any) === BaseGroupRoles.USER; + const isMemberOwner = member.role === GroupRoles.OWNER; + const isMemberAdmin = member.role === GroupRoles.ADMIN; + const isMemberUser = member.role === GroupRoles.USER; const handleKickFromGroup = () => { dispatch(openModal('CONFIRM', { @@ -78,39 +77,41 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { heading: intl.formatMessage(messages.blockFromGroupHeading), message: intl.formatMessage(messages.blockFromGroupMessage, { name: account.username }), confirm: intl.formatMessage(messages.blockConfirm), - onConfirm: () => blockGroupMember({ account_ids: [member.account.id] }).then(() => { - dispatch(deleteEntities([member.id], Entities.GROUP_MEMBERSHIPS)); - toast.success(intl.formatMessage(messages.blocked, { name: account.acct })); - }), + onConfirm: () => { + blockGroupMember({ account_ids: [member.account.id] }, { + onSuccess() { + dispatch(deleteEntities([member.id], Entities.GROUP_MEMBERSHIPS)); + 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(group.id, account.id, role)).then(() => - toast.success(intl.formatMessage(role === 'admin' ? messages.promotedToAdmin : messages.promotedToMod, { name: account.acct })), - ), - })); - } else { - return dispatch(groupPromoteAccount(group.id, account.id, role)).then(() => - toast.success(intl.formatMessage(role === 'admin' ? messages.promotedToAdmin : messages.promotedToMod, { name: account.acct })), - ); - } + const handleAdminAssignment = () => { + dispatch(openModal('CONFIRM', { + heading: intl.formatMessage(messages.promoteConfirm), + message: intl.formatMessage(messages.promoteConfirmMessage, { name: account.username }), + confirm: intl.formatMessage(messages.promoteConfirm), + confirmationTheme: 'primary', + onConfirm: () => { + promoteGroupMember({ role: GroupRoles.ADMIN, account_ids: [account.id] }, { + onSuccess() { + toast.success( + intl.formatMessage(messages.promotedToAdmin, { name: account.acct }), + ); + }, + }); + }, + })); }; - const handlePromoteToGroupAdmin = () => onPromote('admin', true); - - const handlePromoteToGroupMod = () => { - onPromote('moderator', group.relationship!.role === 'moderator'); - }; - - const handleDemote = () => { - dispatch(groupDemoteAccount(group.id, account.id, 'user')).then(() => - toast.success(intl.formatMessage(messages.demotedToUser, { name: account.acct })), - ).catch(() => {}); + const handleUserAssignment = () => { + demoteGroupMember({ role: GroupRoles.USER, account_ids: [account.id] }, { + onSuccess() { + toast.success(intl.formatMessage(messages.demotedToUser, { name: account.acct })); + }, + }); }; const menu: IMenu = useMemo(() => { @@ -120,9 +121,26 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { return items; } + if (isCurrentUserOwner) { + if (isMemberUser) { + items.push({ + text: intl.formatMessage(messages.groupModPromoteMod, { role: GroupRoles.ADMIN }), + icon: require('@tabler/icons/briefcase.svg'), + action: handleAdminAssignment, + }); + } else if (isMemberAdmin) { + items.push({ + text: intl.formatMessage(messages.groupModDemote, { role: GroupRoles.ADMIN, name: account.username }), + icon: require('@tabler/icons/briefcase.svg'), + action: handleUserAssignment, + destructive: true, + }); + } + } + if ( - (isCurrentUserAdmin || isCurrentUserModerator) && - (isMemberModerator || isMemberUser) && + (isCurrentUserOwner || isCurrentUserAdmin) && + (isMemberAdmin || isMemberUser) && member.role !== group.relationship.role ) { if (features.groupsKick) { @@ -141,29 +159,6 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { }); } - if (isCurrentUserAdmin && !isMemberAdmin && account.acct === account.username) { - items.push(null); - - if (isMemberModerator) { - items.push({ - text: intl.formatMessage(messages.groupModPromoteAdmin, { name: account.username }), - icon: require('@tabler/icons/arrow-up-circle.svg'), - action: handlePromoteToGroupAdmin, - }); - items.push({ - text: intl.formatMessage(messages.groupModDemote, { name: account.username }), - icon: require('@tabler/icons/arrow-down-circle.svg'), - action: handleDemote, - }); - } else if (isMemberUser) { - items.push({ - text: intl.formatMessage(messages.groupModPromoteMod, { name: account.username }), - icon: require('@tabler/icons/arrow-up-circle.svg'), - action: handlePromoteToGroupMod, - }); - } - } - return items; }, [group, account]); @@ -174,12 +169,12 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => { - {(isMemberAdmin || isMemberModerator) ? ( + {(isMemberOwner || isMemberAdmin) ? ( diff --git a/app/soapbox/features/group/group-members.tsx b/app/soapbox/features/group/group-members.tsx index 99ae8c28a..9fa1d135f 100644 --- a/app/soapbox/features/group/group-members.tsx +++ b/app/soapbox/features/group/group-members.tsx @@ -2,8 +2,8 @@ import React, { useMemo } from 'react'; import ScrollableList from 'soapbox/components/scrollable-list'; import { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers'; -import { useGroupRoles } from 'soapbox/hooks/useGroupRoles'; import { useGroup } from 'soapbox/queries/groups'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import PlaceholderAccount from '../placeholder/components/placeholder-account'; @@ -16,22 +16,20 @@ interface IGroupMembers { } const GroupMembers: React.FC = (props) => { - const { roles: { admin, moderator, user } } = useGroupRoles(); - const groupId = props.params.id; const { group, isFetching: isFetchingGroup } = useGroup(groupId); - const { groupMembers: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, admin); - const { groupMembers: moderators, isFetching: isFetchingModerators } = useGroupMembers(groupId, moderator); - const { groupMembers: users, isFetching: isFetchingUsers, fetchNextPage, hasNextPage } = useGroupMembers(groupId, user); + const { groupMembers: owners, isFetching: isFetchingOwners } = useGroupMembers(groupId, GroupRoles.OWNER); + const { groupMembers: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, GroupRoles.ADMIN); + const { groupMembers: users, isFetching: isFetchingUsers, fetchNextPage, hasNextPage } = useGroupMembers(groupId, GroupRoles.USER); - const isLoading = isFetchingGroup || isFetchingAdmins || isFetchingModerators || isFetchingUsers; + const isLoading = isFetchingGroup || isFetchingOwners || isFetchingAdmins || isFetchingUsers; const members = useMemo(() => [ + ...owners, ...admins, - ...moderators, ...users, - ], [admins, moderators, users]); + ], [owners, admins, users]); return ( <> diff --git a/app/soapbox/hooks/api/groups/useBlockGroupMember.ts b/app/soapbox/hooks/api/groups/useBlockGroupMember.ts index 12af739c9..36f722f27 100644 --- a/app/soapbox/hooks/api/groups/useBlockGroupMember.ts +++ b/app/soapbox/hooks/api/groups/useBlockGroupMember.ts @@ -4,7 +4,7 @@ import { useEntityActions } from 'soapbox/entity-store/hooks'; import type { Group, GroupMember } from 'soapbox/schemas'; function useBlockGroupMember(group: Group, groupMember: GroupMember) { - const { createEntity } = useEntityActions( + const { createEntity } = useEntityActions( [Entities.GROUP_MEMBERSHIPS, groupMember.id], { post: `/api/v1/groups/${group.id}/blocks` }, ); diff --git a/app/soapbox/hooks/api/groups/useDemoteGroupMember.ts b/app/soapbox/hooks/api/groups/useDemoteGroupMember.ts new file mode 100644 index 000000000..db38164b7 --- /dev/null +++ b/app/soapbox/hooks/api/groups/useDemoteGroupMember.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntityActions } from 'soapbox/entity-store/hooks'; +import { groupMemberSchema } from 'soapbox/schemas'; + +import type { Group, GroupMember } from 'soapbox/schemas'; + +function useDemoteGroupMember(group: Group, groupMember: GroupMember) { + const { createEntity } = useEntityActions( + [Entities.GROUP_MEMBERSHIPS, groupMember.id], + { post: `/api/v1/groups/${group.id}/demote` }, + { schema: z.array(groupMemberSchema).transform((arr) => arr[0]) }, + ); + + return createEntity; +} + +export { useDemoteGroupMember }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/usePromoteGroupMember.ts b/app/soapbox/hooks/api/groups/usePromoteGroupMember.ts new file mode 100644 index 000000000..148980f0c --- /dev/null +++ b/app/soapbox/hooks/api/groups/usePromoteGroupMember.ts @@ -0,0 +1,19 @@ +import { z } from 'zod'; + +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntityActions } from 'soapbox/entity-store/hooks'; +import { groupMemberSchema } from 'soapbox/schemas'; + +import type { Group, GroupMember } from 'soapbox/schemas'; + +function usePromoteGroupMember(group: Group, groupMember: GroupMember) { + const { createEntity } = useEntityActions( + [Entities.GROUP_MEMBERSHIPS, groupMember.id], + { post: `/api/v1/groups/${group.id}/promote` }, + { schema: z.array(groupMemberSchema).transform((arr) => arr[0]) }, + ); + + return createEntity; +} + +export { usePromoteGroupMember }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts new file mode 100644 index 000000000..144196a2c --- /dev/null +++ b/app/soapbox/hooks/api/index.ts @@ -0,0 +1,6 @@ +/** + * Groups + */ +export { useBlockGroupMember } from './groups/useBlockGroupMember'; +export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; +export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; \ No newline at end of file diff --git a/app/soapbox/hooks/useGroupRoles.ts b/app/soapbox/hooks/useGroupRoles.ts deleted file mode 100644 index dd435ce16..000000000 --- a/app/soapbox/hooks/useGroupRoles.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { TRUTHSOCIAL } from 'soapbox/utils/features'; - -import { useBackend } from './useBackend'; - -enum TruthSocialGroupRoles { - ADMIN = 'owner', - MODERATOR = 'admin', - USER = 'user' -} - -enum BaseGroupRoles { - ADMIN = 'admin', - MODERATOR = 'moderator', - USER = 'user' -} - -const roleMap = { - [TruthSocialGroupRoles.ADMIN]: BaseGroupRoles.ADMIN, - [TruthSocialGroupRoles.MODERATOR]: BaseGroupRoles.MODERATOR, - [TruthSocialGroupRoles.USER]: BaseGroupRoles.USER, -}; - -/** - * Returns the correct role name depending on the used backend. - * - * @returns Object - */ -const useGroupRoles = () => { - const version = useBackend(); - const isTruthSocial = version.software === TRUTHSOCIAL; - const selectedRoles = isTruthSocial ? TruthSocialGroupRoles : BaseGroupRoles; - - const normalizeRole = (role: TruthSocialGroupRoles) => { - if (isTruthSocial) { - return roleMap[role]; - } - - return role; - }; - - return { - normalizeRole, - roles: { - admin: selectedRoles.ADMIN, - moderator: selectedRoles.MODERATOR, - user: selectedRoles.USER, - }, - }; -}; - -export { useGroupRoles, TruthSocialGroupRoles, BaseGroupRoles }; \ No newline at end of file diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index f9ce366ae..0c0b8aff0 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -470,8 +470,8 @@ "confirmations.block.confirm": "Block", "confirmations.block.heading": "Block @{name}", "confirmations.block.message": "Are you sure you want to block {name}?", - "confirmations.block_from_group.confirm": "Block", - "confirmations.block_from_group.heading": "Block group member", + "confirmations.block_from_group.confirm": "Ban User", + "confirmations.block_from_group.heading": "Ban From Group", "confirmations.block_from_group.message": "Are you sure you want to ban @{name} from the group?", "confirmations.cancel.confirm": "Discard", "confirmations.cancel.heading": "Discard post", @@ -509,8 +509,6 @@ "confirmations.mute.confirm": "Mute", "confirmations.mute.heading": "Mute @{name}", "confirmations.mute.message": "Are you sure you want to mute {name}?", - "confirmations.promote_in_group.confirm": "Promote", - "confirmations.promote_in_group.message": "Are you sure you want to promote @{name}? You will not be able to demote them.", "confirmations.redraft.confirm": "Delete & redraft", "confirmations.redraft.heading": "Delete & redraft", "confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.", @@ -768,18 +766,15 @@ "gdpr.title": "{siteTitle} uses cookies", "getting_started.open_source_notice": "{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).", "group.cancel_request": "Cancel Request", + "group.demote.user.success": "@{name} is now a member", "group.group_mod_authorize": "Accept", "group.group_mod_authorize.success": "Accepted @{name} to group", "group.group_mod_block": "Ban from group", - "group.group_mod_block.success": "You have successfully blocked @{name} from the group", - "group.group_mod_demote": "Demote @{name}", - "group.group_mod_demote.success": "Demoted @{name} to group user", + "group.group_mod_block.success": "@{name} is banned", + "group.group_mod_demote": "Remove {role} role", "group.group_mod_kick": "Kick @{name} from group", "group.group_mod_kick.success": "Kicked @{name} from group", - "group.group_mod_promote_admin": "Promote @{name} to group administrator", - "group.group_mod_promote_admin.success": "Promoted @{name} to group administrator", - "group.group_mod_promote_mod": "Promote @{name} to group moderator", - "group.group_mod_promote_mod.success": "Promoted @{name} to group moderator", + "group.group_mod_promote_mod": "Assign {role} role", "group.group_mod_reject": "Reject", "group.group_mod_reject.success": "Rejected @{name} from group", "group.group_mod_unblock": "Unblock", @@ -798,6 +793,9 @@ "group.privacy.public": "Public", "group.privacy.public.full": "Public Group", "group.privacy.public.info": "Discoverable. Anyone can join.", + "group.promote.admin.confirmation.message": "Are you sure you want to assign the admin role to @{name}?", + "group.promote.admin.confirmation.title": "Assign Admin Role", + "group.promote.admin.success": "@{name} is now an admin", "group.role.admin": "Admin", "group.role.moderator": "Moderator", "group.tabs.all": "All", diff --git a/app/soapbox/queries/groups/members.ts b/app/soapbox/queries/groups/members.ts index d1707fb6c..4d0a0bb66 100644 --- a/app/soapbox/queries/groups/members.ts +++ b/app/soapbox/queries/groups/members.ts @@ -1,14 +1,14 @@ import { useQuery } from '@tanstack/react-query'; import { useApi } from 'soapbox/hooks'; -import { useGroupRoles } from 'soapbox/hooks/useGroupRoles'; import { normalizeAccount } from 'soapbox/normalizers'; +import { GroupRoles } from 'soapbox/schemas/group-member'; const GroupMemberKeys = { members: (id: string, role: string) => ['group', id, role] as const, }; -const useGroupMembers = (groupId: string, role: ReturnType['roles']['admin']) => { +const useGroupMembers = (groupId: string, role: GroupRoles) => { const api = useApi(); const getQuery = async () => { diff --git a/app/soapbox/schemas/group-member.ts b/app/soapbox/schemas/group-member.ts index 73e051588..4521450cb 100644 --- a/app/soapbox/schemas/group-member.ts +++ b/app/soapbox/schemas/group-member.ts @@ -2,27 +2,18 @@ import z from 'zod'; import { accountSchema } from './account'; -enum TruthSocialGroupRoles { - ADMIN = 'owner', - MODERATOR = 'admin', - USER = 'user' -} - -enum BaseGroupRoles { +enum GroupRoles { + OWNER = 'owner', ADMIN = 'admin', - MODERATOR = 'moderator', USER = 'user' } const groupMemberSchema = z.object({ id: z.string(), account: accountSchema, - role: z.union([ - z.nativeEnum(TruthSocialGroupRoles), - z.nativeEnum(BaseGroupRoles), - ]), + role: z.nativeEnum(GroupRoles), }); type GroupMember = z.infer; -export { groupMemberSchema, GroupMember }; \ No newline at end of file +export { groupMemberSchema, GroupMember, GroupRoles }; \ No newline at end of file diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index b435406aa..c07fe5b60 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -532,9 +532,14 @@ const getInstanceFeatures = (instance: Instance) => { /** * Can query pending Group requests. - */ + */ groupsPending: v.software === TRUTHSOCIAL, + /** + * Can promote members to Admins. + */ + groupsPromoteToAdmin: v.software !== TRUTHSOCIAL, + /** * Can hide follows/followers lists and counts. * @see PATCH /api/v1/accounts/update_credentials