From 8a36561ec8165b7e9d24eb55601d55fae2e1bd52 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 13 Mar 2023 09:47:23 -0400 Subject: [PATCH] Use entities with Group Members --- app/soapbox/entity-store/entities.ts | 3 + .../group/components/group-header.tsx | 8 +- .../components/group-member-list-item.tsx | 219 +++++++++++++ app/soapbox/features/group/group-members.tsx | 298 +++--------------- app/soapbox/hooks/api/useGroupMembers.ts | 36 +++ app/soapbox/hooks/useGroupRoles.ts | 51 +++ app/soapbox/hooks/useVersion.ts | 16 + app/soapbox/normalizers/group-member.ts | 21 ++ app/soapbox/normalizers/index.ts | 1 + app/soapbox/queries/groups.ts | 2 +- app/soapbox/queries/groups/members.ts | 40 +++ app/soapbox/types/entities.ts | 1 + 12 files changed, 432 insertions(+), 264 deletions(-) create mode 100644 app/soapbox/entity-store/entities.ts create mode 100644 app/soapbox/features/group/components/group-member-list-item.tsx create mode 100644 app/soapbox/hooks/api/useGroupMembers.ts create mode 100644 app/soapbox/hooks/useGroupRoles.ts create mode 100644 app/soapbox/hooks/useVersion.ts create mode 100644 app/soapbox/normalizers/group-member.ts create mode 100644 app/soapbox/queries/groups/members.ts diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts new file mode 100644 index 000000000..b196baadf --- /dev/null +++ b/app/soapbox/entity-store/entities.ts @@ -0,0 +1,3 @@ +export enum Entities { + GROUP_MEMBERSHIPS = 'GroupMemberships' +} \ No newline at end of file diff --git a/app/soapbox/features/group/components/group-header.tsx b/app/soapbox/features/group/components/group-header.tsx index 4e1c50160..4f7065dc0 100644 --- a/app/soapbox/features/group/components/group-header.tsx +++ b/app/soapbox/features/group/components/group-header.tsx @@ -118,7 +118,7 @@ const GroupHeader: React.FC = ({ group }) => { - + = ({ group }) => { - + diff --git a/app/soapbox/features/group/components/group-member-list-item.tsx b/app/soapbox/features/group/components/group-member-list-item.tsx new file mode 100644 index 000000000..6ee6c5c59 --- /dev/null +++ b/app/soapbox/features/group/components/group-member-list-item.tsx @@ -0,0 +1,219 @@ +import clsx from 'clsx'; +import React, { useMemo } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import { groupBlock, groupDemoteAccount, groupKick, groupPromoteAccount } from 'soapbox/actions/groups'; +import { openModal } from 'soapbox/actions/modals'; +import Account from 'soapbox/components/account'; +import { HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui'; +import SvgIcon from 'soapbox/components/ui/icon/svg-icon'; +import { useAccount, useAppDispatch } from 'soapbox/hooks'; +import { BaseGroupRoles, useGroupRoles } from 'soapbox/hooks/useGroupRoles'; +import { GroupMember } from 'soapbox/normalizers/group-member'; +import toast from 'soapbox/toast'; + +import type { Menu as IMenu } from 'soapbox/components/dropdown-menu'; +import type { Account as AccountEntity, Group } from 'soapbox/types/entities'; + +const messages = defineMessages({ + blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' }, + blockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to block @{name} from interacting with this group?' }, + blocked: { id: 'group.group_mod_block.success', defaultMessage: 'Blocked @{name} from group' }, + demotedToUser: { id: 'group.group_mod_demote.success', defaultMessage: 'Demoted @{name} to group user' }, + groupModBlock: { id: 'group.group_mod_block', defaultMessage: 'Block @{name} from group' }, + groupModDemote: { id: 'group.group_mod_demote', defaultMessage: 'Demote @{name}' }, + 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' }, + 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' }, +}); + +interface IGroupMemberListItem { + member: GroupMember + group: Group +} + +const GroupMemberListItem = (props: IGroupMemberListItem) => { + const { member, group } = props; + + const dispatch = useAppDispatch(); + const intl = useIntl(); + + const { normalizeRole } = useGroupRoles(); + + 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; + + // 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 handleKickFromGroup = () => { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.kickFromGroupMessage, { name: account.username }), + confirm: intl.formatMessage(messages.kickConfirm), + onConfirm: () => dispatch(groupKick(group.id, 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(group.id, 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(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 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 menu: IMenu = useMemo(() => { + const items: IMenu = []; + + if (!group || !account || !group.relationship?.role) { + return items; + } + + if ( + (isCurrentUserAdmin || isCurrentUserModerator) && + (isMemberModerator || isMemberUser) && + member.role !== group.relationship.role + ) { + items.push({ + text: intl.formatMessage(messages.groupModKick, { name: account.username }), + icon: require('@tabler/icons/user-minus.svg'), + action: handleKickFromGroup, + }); + items.push({ + text: intl.formatMessage(messages.groupModBlock, { name: account.username }), + icon: require('@tabler/icons/ban.svg'), + action: handleBlockFromGroup, + }); + } + + 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]); + + return ( + +
+ +
+ + + {(isMemberAdmin || isMemberModerator) ? ( + + {member.role} + + ) : null} + + {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.target || '_self' }; + + return ( + + + {menuItem.icon && ( + + )} + +
{menuItem.text}
+
+
+ ); + } + })} +
+
+ )} +
+
+ ); +}; + +export default GroupMemberListItem; \ No newline at end of file diff --git a/app/soapbox/features/group/group-members.tsx b/app/soapbox/features/group/group-members.tsx index 3b8482ec4..d92676520 100644 --- a/app/soapbox/features/group/group-members.tsx +++ b/app/soapbox/features/group/group-members.tsx @@ -1,283 +1,59 @@ -import debounce from 'lodash/debounce'; -import React, { useCallback, useEffect } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; -import { Link } from 'react-router-dom'; +import React, { useMemo } from 'react'; -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 { useGroupMembers } from 'soapbox/hooks/api/useGroupMembers'; +import { useGroupRoles } from 'soapbox/hooks/useGroupRoles'; +import { useGroup } from 'soapbox/queries/groups'; 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'; +import GroupMemberListItem from './components/group-member-list-item'; -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.target || '_self' }; - - return ( - - - {menuItem.icon && ( - - )} - -
{menuItem.text}
-
-
- ); - } - })} -
-
- )} -
- ); -}; +import type { Group } from 'soapbox/types/entities'; interface IGroupMembers { - params: RouteParams + params: { id: string } } const GroupMembers: React.FC = (props) => { - const intl = useIntl(); - const dispatch = useAppDispatch(); + const { roles: { admin, moderator, user } } = useGroupRoles(); 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 { 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 handleLoadMore = (role: 'admin' | 'moderator' | 'user') => { - dispatch(expandGroupMemberships(groupId, role)); - }; + const isLoading = isFetchingGroup || isFetchingAdmins || isFetchingModerators || isFetchingUsers; - 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]); + const members = useMemo(() => [ + ...admins, + ...moderators, + ...users, + ], [admins, moderators, users]); return ( <> - {renderMemberships(admins, 'admin', handleLoadMoreAdmins)} - {renderMemberships(moderators, 'moderator', handleLoadMoreModerators)} - {renderMemberships(users, 'user', handleLoadMoreUsers)} + + {members.map((member) => ( + + ))} + ); }; diff --git a/app/soapbox/hooks/api/useGroupMembers.ts b/app/soapbox/hooks/api/useGroupMembers.ts new file mode 100644 index 000000000..24e112ffa --- /dev/null +++ b/app/soapbox/hooks/api/useGroupMembers.ts @@ -0,0 +1,36 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { normalizeAccount } from 'soapbox/normalizers'; +import { Account } from 'soapbox/types/entities'; + +import { BaseGroupRoles, TruthSocialGroupRoles } from '../useGroupRoles'; + +interface GroupMember { + id: string + role: BaseGroupRoles | TruthSocialGroupRoles + account: Account | any +} + +const normalizeGroupMember = (groupMember: GroupMember): GroupMember => { + return { + ...groupMember, + account: normalizeAccount(groupMember.account), + }; +}; + +const parseGroupMember = (entity: unknown) => entity ? normalizeGroupMember(entity as GroupMember) : undefined; + +function useGroupMembers(groupId: string, role: string) { + const { entities, ...result } = useEntities( + [Entities.GROUP_MEMBERSHIPS, `${groupId}:${role}`], + `/api/v1/groups/${groupId}/memberships?role=${role}&limit=1`, + { parser: parseGroupMember }, + ); + + return { + ...result, + groupMembers: entities, + }; +} + +export { useGroupMembers }; \ No newline at end of file diff --git a/app/soapbox/hooks/useGroupRoles.ts b/app/soapbox/hooks/useGroupRoles.ts new file mode 100644 index 000000000..eba96a570 --- /dev/null +++ b/app/soapbox/hooks/useGroupRoles.ts @@ -0,0 +1,51 @@ +import { TRUTHSOCIAL } from 'soapbox/utils/features'; + +import { useVersion } from './useVersion'; + +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 = useVersion(); + 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/hooks/useVersion.ts b/app/soapbox/hooks/useVersion.ts new file mode 100644 index 000000000..2ed75f5da --- /dev/null +++ b/app/soapbox/hooks/useVersion.ts @@ -0,0 +1,16 @@ +import { parseVersion } from 'soapbox/utils/features'; + +import { useInstance } from './useInstance'; + +/** + * Get the Backend version. + * + * @returns Backend + */ +const useVersion = () => { + const instance = useInstance(); + + return parseVersion(instance.version); +}; + +export { useVersion }; \ No newline at end of file diff --git a/app/soapbox/normalizers/group-member.ts b/app/soapbox/normalizers/group-member.ts new file mode 100644 index 000000000..0f2dc97f9 --- /dev/null +++ b/app/soapbox/normalizers/group-member.ts @@ -0,0 +1,21 @@ +/** + * Group Member normalizer: + * Converts API group members into our internal format. + */ +import { BaseGroupRoles, TruthSocialGroupRoles } from 'soapbox/hooks/useGroupRoles'; +import { Account } from 'soapbox/types/entities'; + +import { normalizeAccount } from './account'; + +export interface GroupMember { + id: string + role: BaseGroupRoles | TruthSocialGroupRoles + account: Account | any +} + +export const normalizeGroupMember = (groupMember: GroupMember): GroupMember => { + return { + ...groupMember, + account: normalizeAccount(groupMember.account), + }; +}; diff --git a/app/soapbox/normalizers/index.ts b/app/soapbox/normalizers/index.ts index 004049988..c4f4883bc 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -13,6 +13,7 @@ export { FilterRecord, normalizeFilter } from './filter'; export { FilterKeywordRecord, normalizeFilterKeyword } from './filter-keyword'; export { FilterStatusRecord, normalizeFilterStatus } from './filter-status'; export { normalizeGroup } from './group'; +// export { GroupMember, normalizeGroupMember } from './group-member'; export { GroupRelationshipRecord, normalizeGroupRelationship } from './group-relationship'; export { HistoryRecord, normalizeHistory } from './history'; export { InstanceRecord, normalizeInstance } from './instance'; diff --git a/app/soapbox/queries/groups.ts b/app/soapbox/queries/groups.ts index aa8635183..fd986bf02 100644 --- a/app/soapbox/queries/groups.ts +++ b/app/soapbox/queries/groups.ts @@ -200,7 +200,7 @@ const useGroup = (id: string) => { return groups[0]; }; - const queryInfo = useQuery(GroupKeys.group(id), getGroup, { + const queryInfo = useQuery(GroupKeys.group(id), getGroup, { enabled: features.groups && !!id, }); diff --git a/app/soapbox/queries/groups/members.ts b/app/soapbox/queries/groups/members.ts new file mode 100644 index 000000000..d1707fb6c --- /dev/null +++ b/app/soapbox/queries/groups/members.ts @@ -0,0 +1,40 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks'; +import { useGroupRoles } from 'soapbox/hooks/useGroupRoles'; +import { normalizeAccount } from 'soapbox/normalizers'; + +const GroupMemberKeys = { + members: (id: string, role: string) => ['group', id, role] as const, +}; + +const useGroupMembers = (groupId: string, role: ReturnType['roles']['admin']) => { + const api = useApi(); + + const getQuery = async () => { + const { data } = await api.get(`/api/v1/groups/${groupId}/memberships`, { + params: { + role, + }, + }); + + const result = data.map((member: any) => { + return { + ...member, + account: normalizeAccount(member.account), + }; + }); + + return result; + }; + + return useQuery( + GroupMemberKeys.members(groupId, role), + getQuery, + { + placeholderData: [], + }, + ); +}; + +export { useGroupMembers }; \ No newline at end of file diff --git a/app/soapbox/types/entities.ts b/app/soapbox/types/entities.ts index 61691a54a..c4d8907c5 100644 --- a/app/soapbox/types/entities.ts +++ b/app/soapbox/types/entities.ts @@ -111,5 +111,6 @@ export { export type { Group, + GroupMember, GroupRelationship, } from 'soapbox/schemas'; \ No newline at end of file