From 11d06e6b6ea89f7e977755e1f237f69abdfef9db Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 10 Mar 2023 11:29:31 -0600 Subject: [PATCH 01/12] useEntities: support multiple list keys --- app/soapbox/entity-store/hooks/useEntities.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index 1d1cd5eed..4f50bc566 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -14,8 +14,12 @@ import type { RootState } from 'soapbox/store'; type EntityPath = [ /** Name of the entity type for use in the global cache, eg `'Notification'`. */ entityType: string, - /** Name of a particular index of this entity type. You can use empty-string (`''`) if you don't need separate lists. */ - listKey: string, + /** + * Name of a particular index of this entity type. + * Multiple params get combined into one string with a `:` separator. + * You can use empty-string (`''`) if you don't need separate lists. + */ + ...listKeys: string[], ] /** Additional options for the hook. */ @@ -42,7 +46,8 @@ function useEntities( const dispatch = useAppDispatch(); const getState = useGetState(); - const [entityType, listKey] = path; + const [entityType, ...listKeys] = path; + const listKey = listKeys.join(':'); const entities = useAppSelector(state => selectEntities(state, path)); const isFetching = useListState(path, 'fetching'); From 8a36561ec8165b7e9d24eb55601d55fae2e1bd52 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 13 Mar 2023 09:47:23 -0400 Subject: [PATCH 02/12] 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 From 08f97a133eb26c38ccbb200ed4b979246a940818 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 13 Mar 2023 13:23:40 -0400 Subject: [PATCH 03/12] Update blankslate for Group Timeline --- app/soapbox/features/group/group-timeline.tsx | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx index f80343c1e..f549d5605 100644 --- a/app/soapbox/features/group/group-timeline.tsx +++ b/app/soapbox/features/group/group-timeline.tsx @@ -5,9 +5,9 @@ import { Link } from 'react-router-dom'; import { groupCompose } from 'soapbox/actions/compose'; import { connectGroupStream } from 'soapbox/actions/streaming'; import { expandGroupTimeline } from 'soapbox/actions/timelines'; -import { Avatar, HStack, Stack } from 'soapbox/components/ui'; +import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui'; import ComposeForm from 'soapbox/features/compose/components/compose-form'; -import { useAppDispatch, useAppSelector, useOwnAccount } from 'soapbox/hooks'; +import { useAppDispatch, useGroup, useOwnAccount } from 'soapbox/hooks'; import Timeline from '../ui/components/timeline'; @@ -23,7 +23,9 @@ const GroupTimeline: React.FC = (props) => { const groupId = props.params.id; - const relationship = useAppSelector((state) => state.group_relationships.get(groupId)); + const { group } = useGroup(groupId); + + const canComposeGroupStatus = !!account && group?.relationship?.member; const handleLoadMore = (maxId: string) => { dispatch(expandGroupTimeline(groupId, { maxId })); @@ -41,9 +43,13 @@ const GroupTimeline: React.FC = (props) => { }; }, [groupId]); + if (!group) { + return null; + } + return ( - {!!account && relationship?.member && ( + {canComposeGroupStatus && (
@@ -59,11 +65,26 @@ const GroupTimeline: React.FC = (props) => {
)} + } + emptyMessage={ + +
+ +
+ + + + +
+ } + emptyMessageCard={false} divideType='border' showGroup={false} /> From 9d1c2df1a21063218739a481f14c91cb096a0e0c Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 13 Mar 2023 15:08:02 -0400 Subject: [PATCH 04/12] Use ZOD for group-members --- .../components/group-member-list-item.tsx | 3 +-- app/soapbox/hooks/api/useGroupMembers.ts | 25 +++++------------- app/soapbox/normalizers/group-member.ts | 21 --------------- app/soapbox/normalizers/index.ts | 1 - app/soapbox/schemas/group-member.ts | 26 +++++++++++++++++++ app/soapbox/schemas/index.ts | 2 ++ 6 files changed, 36 insertions(+), 42 deletions(-) delete mode 100644 app/soapbox/normalizers/group-member.ts create mode 100644 app/soapbox/schemas/group-member.ts 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 6ee6c5c59..76ebc1863 100644 --- a/app/soapbox/features/group/components/group-member-list-item.tsx +++ b/app/soapbox/features/group/components/group-member-list-item.tsx @@ -10,11 +10,10 @@ import { HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, 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'; +import type { Account as AccountEntity, Group, GroupMember } from 'soapbox/types/entities'; const messages = defineMessages({ blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Block' }, diff --git a/app/soapbox/hooks/api/useGroupMembers.ts b/app/soapbox/hooks/api/useGroupMembers.ts index 24e112ffa..305a969d5 100644 --- a/app/soapbox/hooks/api/useGroupMembers.ts +++ b/app/soapbox/hooks/api/useGroupMembers.ts @@ -1,28 +1,17 @@ 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 { GroupMember, groupMemberSchema } from 'soapbox/schemas'; -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) => { + const result = groupMemberSchema.safeParse(entity); + if (result.success) { + return result.data; + } }; -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}`], + [Entities.GROUP_MEMBERSHIPS, groupId, role], `/api/v1/groups/${groupId}/memberships?role=${role}&limit=1`, { parser: parseGroupMember }, ); diff --git a/app/soapbox/normalizers/group-member.ts b/app/soapbox/normalizers/group-member.ts deleted file mode 100644 index 0f2dc97f9..000000000 --- a/app/soapbox/normalizers/group-member.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * 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 c4f4883bc..004049988 100644 --- a/app/soapbox/normalizers/index.ts +++ b/app/soapbox/normalizers/index.ts @@ -13,7 +13,6 @@ 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/schemas/group-member.ts b/app/soapbox/schemas/group-member.ts new file mode 100644 index 000000000..99340911f --- /dev/null +++ b/app/soapbox/schemas/group-member.ts @@ -0,0 +1,26 @@ +import z from 'zod'; + +enum TruthSocialGroupRoles { + ADMIN = 'owner', + MODERATOR = 'admin', + USER = 'user' +} + +enum BaseGroupRoles { + ADMIN = 'admin', + MODERATOR = 'moderator', + USER = 'user' +} + +const groupMemberSchema = z.object({ + id: z.string(), + account: z.any(), + role: z.union([ + z.nativeEnum(TruthSocialGroupRoles), + z.nativeEnum(BaseGroupRoles), + ]), +}); + +type GroupMember = z.infer; + +export { groupMemberSchema, GroupMember }; \ No newline at end of file diff --git a/app/soapbox/schemas/index.ts b/app/soapbox/schemas/index.ts index f89e714c1..bae2eb830 100644 --- a/app/soapbox/schemas/index.ts +++ b/app/soapbox/schemas/index.ts @@ -2,5 +2,7 @@ export { customEmojiSchema } from './custom-emoji'; export type { CustomEmoji } from './custom-emoji'; export { groupSchema } from './group'; export type { Group } from './group'; +export { groupMemberSchema } from './group-member'; +export type { GroupMember } from './group-member'; export { groupRelationshipSchema } from './group-relationship'; export type { GroupRelationship } from './group-relationship'; From 1d53f48904118b1d5c4b54228b30834aa0b8a701 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 14 Mar 2023 12:55:42 -0400 Subject: [PATCH 05/12] Fix parser --- app/soapbox/hooks/api/useGroupMembers.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/app/soapbox/hooks/api/useGroupMembers.ts b/app/soapbox/hooks/api/useGroupMembers.ts index 305a969d5..8948660d6 100644 --- a/app/soapbox/hooks/api/useGroupMembers.ts +++ b/app/soapbox/hooks/api/useGroupMembers.ts @@ -2,18 +2,11 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntities } from 'soapbox/entity-store/hooks'; import { GroupMember, groupMemberSchema } from 'soapbox/schemas'; -const parseGroupMember = (entity: unknown) => { - const result = groupMemberSchema.safeParse(entity); - if (result.success) { - return result.data; - } -}; - 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 }, + `/api/v1/groups/${groupId}/memberships?role=${role}`, + { schema: groupMemberSchema }, ); return { From 99b7a1bdd7bda1f1d6514362809f7c18e86c05d4 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 14 Mar 2023 13:43:28 -0400 Subject: [PATCH 06/12] Fix i18n --- app/soapbox/locales/en.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 3519ef14a..e354e8422 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -767,7 +767,6 @@ "gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.", "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.admin_subheading": "Group administrators", "group.cancel_request": "Cancel Request", "group.group_mod_authorize": "Accept", "group.group_mod_authorize.success": "Accepted @{name} to group", @@ -793,7 +792,6 @@ "group.leave": "Leave Group", "group.leave.success": "Left the group", "group.manage": "Manage Group", - "group.moderator_subheading": "Group moderators", "group.privacy.locked": "Private", "group.privacy.public": "Public", "group.role.admin": "Admin", @@ -801,7 +799,6 @@ "group.tabs.all": "All", "group.tabs.members": "Members", "group.upload_banner": "Upload photo", - "group.user_subheading": "Users", "groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.", "groups.discover.popular.title": "Popular Groups", "groups.discover.search.error.subtitle": "Please try again later.", From 50dadeb1b8301806a8a6db28b97070508b134dd7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 10 Mar 2023 11:29:31 -0600 Subject: [PATCH 07/12] useEntities: support multiple list keys --- app/soapbox/entity-store/hooks/useEntities.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index 4f50bc566..57e544faa 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -48,6 +48,7 @@ function useEntities( const [entityType, ...listKeys] = path; const listKey = listKeys.join(':'); + const entities = useAppSelector(state => selectEntities(state, path)); const isFetching = useListState(path, 'fetching'); From 7be8218f0c1fa73a525a5a19215a83d591856a3f Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 13 Mar 2023 16:08:42 -0400 Subject: [PATCH 08/12] Convert popular/suggested Groups to use Entities --- app/soapbox/entity-store/entities.ts | 3 +- app/soapbox/entity-store/hooks/useEntities.ts | 20 ++++++--- app/soapbox/entity-store/types.ts | 2 + app/soapbox/entity-store/utils.ts | 3 +- .../components/discover/popular-groups.tsx | 2 +- .../components/discover/suggested-groups.tsx | 2 +- .../panels/suggested-groups-panel.tsx | 2 +- app/soapbox/hooks/api/usePopularGroups.ts | 40 +++++++++++++++++ app/soapbox/hooks/api/useSuggestedGroups.ts | 40 +++++++++++++++++ app/soapbox/hooks/useGroups.ts | 2 +- app/soapbox/queries/groups.ts | 44 ------------------- 11 files changed, 104 insertions(+), 56 deletions(-) create mode 100644 app/soapbox/hooks/api/usePopularGroups.ts create mode 100644 app/soapbox/hooks/api/useSuggestedGroups.ts diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts index b196baadf..886c60113 100644 --- a/app/soapbox/entity-store/entities.ts +++ b/app/soapbox/entity-store/entities.ts @@ -1,3 +1,4 @@ export enum Entities { - GROUP_MEMBERSHIPS = 'GroupMemberships' + GROUP_MEMBERSHIPS = 'GroupMemberships', + POPULAR_GROUPS = 'PopularGroups' } \ No newline at end of file diff --git a/app/soapbox/entity-store/hooks/useEntities.ts b/app/soapbox/entity-store/hooks/useEntities.ts index 57e544faa..3b84f07ca 100644 --- a/app/soapbox/entity-store/hooks/useEntities.ts +++ b/app/soapbox/entity-store/hooks/useEntities.ts @@ -31,6 +31,8 @@ interface UseEntitiesOpts { * It is 1 minute by default, and can be set to `Infinity` to opt-out of automatic fetching. */ staleTime?: number + /** A flag to potentially disable sending requests to the API. */ + enabled?: boolean } /** A hook for fetching and displaying API entities. */ @@ -51,8 +53,11 @@ function useEntities( const entities = useAppSelector(state => selectEntities(state, path)); + const isEnabled = opts.enabled ?? true; const isFetching = useListState(path, 'fetching'); const lastFetchedAt = useListState(path, 'lastFetchedAt'); + const isFetched = useListState(path, 'fetched'); + const isError = !!useListState(path, 'error'); const next = useListState(path, 'next'); const prev = useListState(path, 'prev'); @@ -72,6 +77,7 @@ function useEntities( next: getNextLink(response), prev: getPrevLink(response), fetching: false, + fetched: true, error: null, lastFetchedAt: new Date(), })); @@ -101,20 +107,22 @@ function useEntities( const staleTime = opts.staleTime ?? 60000; useEffect(() => { - if (!isFetching && (!lastFetchedAt || lastFetchedAt.getTime() + staleTime <= Date.now())) { + if (isEnabled && !isFetching && (!lastFetchedAt || lastFetchedAt.getTime() + staleTime <= Date.now())) { fetchEntities(); } - }, [endpoint]); + }, [endpoint, isEnabled]); return { entities, fetchEntities, - isFetching, - isLoading: isFetching && entities.length === 0, - hasNextPage: !!next, - hasPreviousPage: !!prev, fetchNextPage, fetchPreviousPage, + hasNextPage: !!next, + hasPreviousPage: !!prev, + isError, + isFetched, + isFetching, + isLoading: isFetching && entities.length === 0, }; } diff --git a/app/soapbox/entity-store/types.ts b/app/soapbox/entity-store/types.ts index eb2a306a0..0e34b62a5 100644 --- a/app/soapbox/entity-store/types.ts +++ b/app/soapbox/entity-store/types.ts @@ -25,6 +25,8 @@ interface EntityListState { prev: string | undefined /** Error returned from the API, if any. */ error: any + /** Whether data has already been fetched */ + fetched: boolean /** Whether data for this list is currently being fetched. */ fetching: boolean /** Date of the last API fetch for this list. */ diff --git a/app/soapbox/entity-store/utils.ts b/app/soapbox/entity-store/utils.ts index 22e0f0c5b..6e96a9ec6 100644 --- a/app/soapbox/entity-store/utils.ts +++ b/app/soapbox/entity-store/utils.ts @@ -29,8 +29,9 @@ const createList = (): EntityList => ({ state: { next: undefined, prev: undefined, - fetching: false, error: null, + fetched: false, + fetching: false, lastFetchedAt: undefined, }, }); diff --git a/app/soapbox/features/groups/components/discover/popular-groups.tsx b/app/soapbox/features/groups/components/discover/popular-groups.tsx index 63b9067e0..79024466b 100644 --- a/app/soapbox/features/groups/components/discover/popular-groups.tsx +++ b/app/soapbox/features/groups/components/discover/popular-groups.tsx @@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl'; import { Carousel, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; -import { usePopularGroups } from 'soapbox/queries/groups'; +import { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups'; import GroupGridItem from './group-grid-item'; diff --git a/app/soapbox/features/groups/components/discover/suggested-groups.tsx b/app/soapbox/features/groups/components/discover/suggested-groups.tsx index 1372f2f43..20686a7cc 100644 --- a/app/soapbox/features/groups/components/discover/suggested-groups.tsx +++ b/app/soapbox/features/groups/components/discover/suggested-groups.tsx @@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl'; import { Carousel, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; -import { useSuggestedGroups } from 'soapbox/queries/groups'; +import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; import GroupGridItem from './group-grid-item'; diff --git a/app/soapbox/features/ui/components/panels/suggested-groups-panel.tsx b/app/soapbox/features/ui/components/panels/suggested-groups-panel.tsx index a3c5a44d5..395a53f9c 100644 --- a/app/soapbox/features/ui/components/panels/suggested-groups-panel.tsx +++ b/app/soapbox/features/ui/components/panels/suggested-groups-panel.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { Widget } from 'soapbox/components/ui'; import GroupListItem from 'soapbox/features/groups/components/discover/group-list-item'; import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search'; -import { useSuggestedGroups } from 'soapbox/queries/groups'; +import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; const SuggestedGroupsPanel = () => { const { groups, isFetching, isFetched, isError } = useSuggestedGroups(); diff --git a/app/soapbox/hooks/api/usePopularGroups.ts b/app/soapbox/hooks/api/usePopularGroups.ts new file mode 100644 index 000000000..b4b74c7f4 --- /dev/null +++ b/app/soapbox/hooks/api/usePopularGroups.ts @@ -0,0 +1,40 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { Group, groupSchema } from 'soapbox/schemas'; + +import { useFeatures } from '../useFeatures'; +import { useGroupRelationships } from '../useGroups'; + +function usePopularGroups() { + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.POPULAR_GROUPS, ''], + '/api/mock/groups', // '/api/v1/truth/trends/groups' + { + parser: parseGroup, + enabled: features.groupsDiscovery, + }, + ); + + const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +const parseGroup = (entity: unknown) => { + const result = groupSchema.safeParse(entity); + if (result.success) { + return result.data; + } +}; + +export { usePopularGroups }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/useSuggestedGroups.ts b/app/soapbox/hooks/api/useSuggestedGroups.ts new file mode 100644 index 000000000..1022a45b0 --- /dev/null +++ b/app/soapbox/hooks/api/useSuggestedGroups.ts @@ -0,0 +1,40 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { Group, groupSchema } from 'soapbox/schemas'; + +import { useFeatures } from '../useFeatures'; +import { useGroupRelationships } from '../useGroups'; + +function useSuggestedGroups() { + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.POPULAR_GROUPS, ''], + '/api/mock/groups', // '/api/v1/truth/suggestions/groups' + { + parser: parseGroup, + enabled: features.groupsDiscovery, + }, + ); + + const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +const parseGroup = (entity: unknown) => { + const result = groupSchema.safeParse(entity); + if (result.success) { + return result.data; + } +}; + +export { useSuggestedGroups }; \ No newline at end of file diff --git a/app/soapbox/hooks/useGroups.ts b/app/soapbox/hooks/useGroups.ts index 7873c4f5d..4759973c8 100644 --- a/app/soapbox/hooks/useGroups.ts +++ b/app/soapbox/hooks/useGroups.ts @@ -44,4 +44,4 @@ function useGroupRelationships(groupIds: string[]) { }; } -export { useGroup, useGroups }; \ No newline at end of file +export { useGroup, useGroups, useGroupRelationships }; diff --git a/app/soapbox/queries/groups.ts b/app/soapbox/queries/groups.ts index fd986bf02..4ac826b4e 100644 --- a/app/soapbox/queries/groups.ts +++ b/app/soapbox/queries/groups.ts @@ -149,48 +149,6 @@ const usePendingGroups = () => { }; }; -const usePopularGroups = () => { - const features = useFeatures(); - const { fetchGroups } = useGroupsApi(); - - const getQuery = async () => { - const { groups } = await fetchGroups('/api/v1/truth/trends/groups'); - - return groups; - }; - - const queryInfo = useQuery(GroupKeys.popularGroups, getQuery, { - enabled: features.groupsDiscovery, - placeholderData: [], - }); - - return { - groups: queryInfo.data || [], - ...queryInfo, - }; -}; - -const useSuggestedGroups = () => { - const features = useFeatures(); - const { fetchGroups } = useGroupsApi(); - - const getQuery = async () => { - const { groups } = await fetchGroups('/api/v1/truth/suggestions/groups'); - - return groups; - }; - - const queryInfo = useQuery(GroupKeys.suggestedGroups, getQuery, { - enabled: features.groupsDiscovery, - placeholderData: [], - }); - - return { - groups: queryInfo.data || [], - ...queryInfo, - }; -}; - const useGroup = (id: string) => { const features = useFeatures(); const { fetchGroups } = useGroupsApi(); @@ -256,6 +214,4 @@ export { useJoinGroup, useLeaveGroup, usePendingGroups, - usePopularGroups, - useSuggestedGroups, }; From f4d2f42c01ddf4d3c9fa165333b04e3f6c53ed3d Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 14 Mar 2023 10:11:20 -0400 Subject: [PATCH 09/12] Use new schema arg --- app/soapbox/hooks/api/usePopularGroups.ts | 9 +-------- app/soapbox/hooks/api/useSuggestedGroups.ts | 9 +-------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/app/soapbox/hooks/api/usePopularGroups.ts b/app/soapbox/hooks/api/usePopularGroups.ts index b4b74c7f4..e856270a5 100644 --- a/app/soapbox/hooks/api/usePopularGroups.ts +++ b/app/soapbox/hooks/api/usePopularGroups.ts @@ -12,7 +12,7 @@ function usePopularGroups() { [Entities.POPULAR_GROUPS, ''], '/api/mock/groups', // '/api/v1/truth/trends/groups' { - parser: parseGroup, + schema: groupSchema, enabled: features.groupsDiscovery, }, ); @@ -30,11 +30,4 @@ function usePopularGroups() { }; } -const parseGroup = (entity: unknown) => { - const result = groupSchema.safeParse(entity); - if (result.success) { - return result.data; - } -}; - export { usePopularGroups }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/useSuggestedGroups.ts b/app/soapbox/hooks/api/useSuggestedGroups.ts index 1022a45b0..829108469 100644 --- a/app/soapbox/hooks/api/useSuggestedGroups.ts +++ b/app/soapbox/hooks/api/useSuggestedGroups.ts @@ -12,7 +12,7 @@ function useSuggestedGroups() { [Entities.POPULAR_GROUPS, ''], '/api/mock/groups', // '/api/v1/truth/suggestions/groups' { - parser: parseGroup, + schema: groupSchema, enabled: features.groupsDiscovery, }, ); @@ -30,11 +30,4 @@ function useSuggestedGroups() { }; } -const parseGroup = (entity: unknown) => { - const result = groupSchema.safeParse(entity); - if (result.success) { - return result.data; - } -}; - export { useSuggestedGroups }; \ No newline at end of file From 1b542c3ed7998274326ec569a079e2a1e969aa38 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 14 Mar 2023 10:19:34 -0400 Subject: [PATCH 10/12] Use Entities enum --- app/soapbox/entity-store/entities.ts | 5 +++- app/soapbox/hooks/api/useSuggestedGroups.ts | 2 +- app/soapbox/hooks/useGroups.ts | 30 +++++++++++++++++---- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts index 886c60113..05d7b60d9 100644 --- a/app/soapbox/entity-store/entities.ts +++ b/app/soapbox/entity-store/entities.ts @@ -1,4 +1,7 @@ export enum Entities { + GROUPS = 'Groups', + GROUP_RELATIONSHIPS = 'GroupRelationships', GROUP_MEMBERSHIPS = 'GroupMemberships', - POPULAR_GROUPS = 'PopularGroups' + POPULAR_GROUPS = 'PopularGroups', + SUGGESTED_GROUPS = 'SuggestedGroups', } \ No newline at end of file diff --git a/app/soapbox/hooks/api/useSuggestedGroups.ts b/app/soapbox/hooks/api/useSuggestedGroups.ts index 829108469..54477c249 100644 --- a/app/soapbox/hooks/api/useSuggestedGroups.ts +++ b/app/soapbox/hooks/api/useSuggestedGroups.ts @@ -9,7 +9,7 @@ function useSuggestedGroups() { const features = useFeatures(); const { entities, ...result } = useEntities( - [Entities.POPULAR_GROUPS, ''], + [Entities.SUGGESTED_GROUPS, ''], '/api/mock/groups', // '/api/v1/truth/suggestions/groups' { schema: groupSchema, diff --git a/app/soapbox/hooks/useGroups.ts b/app/soapbox/hooks/useGroups.ts index 4759973c8..f31144714 100644 --- a/app/soapbox/hooks/useGroups.ts +++ b/app/soapbox/hooks/useGroups.ts @@ -1,12 +1,20 @@ +import { Entities } from 'soapbox/entity-store/entities'; import { useEntities, useEntity } from 'soapbox/entity-store/hooks'; import { groupSchema, Group } from 'soapbox/schemas/group'; import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship'; function useGroups() { - const { entities, ...result } = useEntities(['Group', ''], '/api/v1/groups', { schema: groupSchema }); + const { entities, ...result } = useEntities( + [Entities.GROUPS, ''], + '/api/v1/groups', + { schema: groupSchema }, + ); const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); - const groups = entities.map((group) => ({ ...group, relationship: relationships[group.id] || null })); + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); return { ...result, @@ -15,7 +23,11 @@ function useGroups() { } function useGroup(groupId: string, refetch = true) { - const { entity: group, ...result } = useEntity(['Group', groupId], `/api/v1/groups/${groupId}`, { schema: groupSchema, refetch }); + const { entity: group, ...result } = useEntity( + [Entities.GROUPS, groupId], + `/api/v1/groups/${groupId}`, + { schema: groupSchema, refetch }, + ); const { entity: relationship } = useGroupRelationship(groupId); return { @@ -25,13 +37,21 @@ function useGroup(groupId: string, refetch = true) { } function useGroupRelationship(groupId: string) { - return useEntity(['GroupRelationship', groupId], `/api/v1/groups/relationships?id[]=${groupId}`, { schema: groupRelationshipSchema }); + return useEntity( + [Entities.GROUP_RELATIONSHIPS, groupId], + `/api/v1/groups/relationships?id[]=${groupId}`, + { schema: groupRelationshipSchema }, + ); } function useGroupRelationships(groupIds: string[]) { const q = groupIds.map(id => `id[]=${id}`).join('&'); const endpoint = groupIds.length ? `/api/v1/groups/relationships?${q}` : undefined; - const { entities, ...result } = useEntities(['GroupRelationship', q], endpoint, { schema: groupRelationshipSchema }); + const { entities, ...result } = useEntities( + [Entities.GROUP_RELATIONSHIPS, q], + endpoint, + { schema: groupRelationshipSchema }, + ); const relationships = entities.reduce>((map, relationship) => { map[relationship.id] = relationship; From 821b90c37263d1a41a2a3a0cdb1b3e978b269c54 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 14 Mar 2023 12:46:16 -0400 Subject: [PATCH 11/12] Add pages for Popular / Suggested Groups --- app/soapbox/components/ui/column/column.tsx | 14 ++- .../components/discover/popular-groups.tsx | 26 ++-- .../components/discover/suggested-groups.tsx | 26 ++-- app/soapbox/features/groups/index.tsx | 10 +- app/soapbox/features/groups/popular.tsx | 114 ++++++++++++++++++ app/soapbox/features/groups/suggested.tsx | 114 ++++++++++++++++++ app/soapbox/features/ui/index.tsx | 4 + .../features/ui/util/async-components.ts | 8 ++ app/soapbox/locales/en.json | 3 + 9 files changed, 297 insertions(+), 22 deletions(-) create mode 100644 app/soapbox/features/groups/popular.tsx create mode 100644 app/soapbox/features/groups/suggested.tsx diff --git a/app/soapbox/components/ui/column/column.tsx b/app/soapbox/components/ui/column/column.tsx index 317cf9841..d6cadec77 100644 --- a/app/soapbox/components/ui/column/column.tsx +++ b/app/soapbox/components/ui/column/column.tsx @@ -7,10 +7,10 @@ import { useSoapboxConfig } from 'soapbox/hooks'; import { Card, CardBody, CardHeader, CardTitle } from '../card/card'; -type IColumnHeader = Pick; +type IColumnHeader = Pick; /** Contains the column title with optional back button. */ -const ColumnHeader: React.FC = ({ label, backHref, className }) => { +const ColumnHeader: React.FC = ({ label, backHref, className, action }) => { const history = useHistory(); const handleBackClick = () => { @@ -29,6 +29,12 @@ const ColumnHeader: React.FC = ({ label, backHref, className }) = return ( + + {action && ( +
+ {action} +
+ )}
); }; @@ -48,11 +54,12 @@ export interface IColumn { ref?: React.Ref /** Children to display in the column. */ children?: React.ReactNode + action?: React.ReactNode } /** A backdrop for the main section of the UI. */ const Column: React.FC = React.forwardRef((props, ref: React.ForwardedRef): JSX.Element => { - const { backHref, children, label, transparent = false, withHeader = true, className } = props; + const { backHref, children, label, transparent = false, withHeader = true, className, action } = props; const soapboxConfig = useSoapboxConfig(); return ( @@ -75,6 +82,7 @@ const Column: React.FC = React.forwardRef((props, ref: React.ForwardedR label={label} backHref={backHref} className={clsx({ 'px-4 pt-4 sm:p-0': transparent })} + action={action} /> )} diff --git a/app/soapbox/features/groups/components/discover/popular-groups.tsx b/app/soapbox/features/groups/components/discover/popular-groups.tsx index 79024466b..c5bf90307 100644 --- a/app/soapbox/features/groups/components/discover/popular-groups.tsx +++ b/app/soapbox/features/groups/components/discover/popular-groups.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Carousel, Stack, Text } from 'soapbox/components/ui'; +import Link from 'soapbox/components/link'; +import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; import { usePopularGroups } from 'soapbox/hooks/api/usePopularGroups'; @@ -15,12 +16,23 @@ const PopularGroups = () => { return ( - - - + + + + + + + + + + + {isEmpty ? ( diff --git a/app/soapbox/features/groups/components/discover/suggested-groups.tsx b/app/soapbox/features/groups/components/discover/suggested-groups.tsx index 20686a7cc..9925a423e 100644 --- a/app/soapbox/features/groups/components/discover/suggested-groups.tsx +++ b/app/soapbox/features/groups/components/discover/suggested-groups.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { Carousel, Stack, Text } from 'soapbox/components/ui'; +import Link from 'soapbox/components/link'; +import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui'; import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover'; import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; @@ -15,12 +16,23 @@ const SuggestedGroups = () => { return ( - - - + + + + + + + + + + + {isEmpty ? ( diff --git a/app/soapbox/features/groups/index.tsx b/app/soapbox/features/groups/index.tsx index 30319160b..c84ddbd53 100644 --- a/app/soapbox/features/groups/index.tsx +++ b/app/soapbox/features/groups/index.tsx @@ -60,9 +60,13 @@ const Groups: React.FC = () => { return ( + {features.groupsDiscovery && ( + + )} + {canCreateGroup && ( )} - {features.groupsDiscovery && ( - - )} - { + const { context, ...rest } = props; + return
; +}); + + +const Popular: React.FC = () => { + const intl = useIntl(); + + const [layout, setLayout] = useState(Layout.LIST); + + const { groups, hasNextPage, fetchNextPage } = usePopularGroups(); + + const handleLoadMore = () => { + if (hasNextPage) { + fetchNextPage(); + } + }; + + const renderGroupList = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + const renderGroupGrid = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + return ( + + + + + + } + > + {layout === Layout.LIST ? ( + renderGroupList(group, index)} + endReached={handleLoadMore} + /> + ) : ( + renderGroupGrid(group, index)} + components={{ + Item: (props) => ( +
+ ), + List: GridList, + }} + endReached={handleLoadMore} + /> + )} + + ); +}; + +export default Popular; diff --git a/app/soapbox/features/groups/suggested.tsx b/app/soapbox/features/groups/suggested.tsx new file mode 100644 index 000000000..8c17fc0af --- /dev/null +++ b/app/soapbox/features/groups/suggested.tsx @@ -0,0 +1,114 @@ +import clsx from 'clsx'; +import React, { useCallback, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso'; + +import { Column, HStack, Icon } from 'soapbox/components/ui'; +import { useSuggestedGroups } from 'soapbox/hooks/api/useSuggestedGroups'; + +import GroupGridItem from './components/discover/group-grid-item'; +import GroupListItem from './components/discover/group-list-item'; + +import type { Group } from 'soapbox/schemas'; + +const messages = defineMessages({ + label: { id: 'groups.popular.label', defaultMessage: 'Suggested Groups' }, +}); + +enum Layout { + LIST = 'LIST', + GRID = 'GRID' +} + +const GridList: Components['List'] = React.forwardRef((props, ref) => { + const { context, ...rest } = props; + return
; +}); + + +const Suggested: React.FC = () => { + const intl = useIntl(); + + const [layout, setLayout] = useState(Layout.LIST); + + const { groups, hasNextPage, fetchNextPage } = useSuggestedGroups(); + + const handleLoadMore = () => { + if (hasNextPage) { + fetchNextPage(); + } + }; + + const renderGroupList = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + const renderGroupGrid = useCallback((group: Group, index: number) => ( +
+ +
+ ), []); + + return ( + + + + + + } + > + {layout === Layout.LIST ? ( + renderGroupList(group, index)} + endReached={handleLoadMore} + /> + ) : ( + renderGroupGrid(group, index)} + components={{ + Item: (props) => ( +
+ ), + List: GridList, + }} + endReached={handleLoadMore} + /> + )} + + ); +}; + +export default Suggested; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 38dc88581..89a156be4 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -118,6 +118,8 @@ import { Events, Groups, GroupsDiscover, + GroupsPopular, + GroupsSuggested, PendingGroupRequests, GroupMembers, GroupTimeline, @@ -289,6 +291,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groups && } {features.groupsDiscovery && } + {features.groupsDiscovery && } + {features.groupsDiscovery && } {features.groupsPending && } {features.groups && } {features.groups && } diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index ec8384884..e9b015d8c 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -550,6 +550,14 @@ export function GroupsDiscover() { return import(/* webpackChunkName: "features/groups/discover" */'../../groups/discover'); } +export function GroupsPopular() { + return import(/* webpackChunkName: "features/groups/discover" */'../../groups/popular'); +} + +export function GroupsSuggested() { + return import(/* webpackChunkName: "features/groups/discover" */'../../groups/suggested'); +} + export function PendingGroupRequests() { return import(/* webpackChunkName: "features/groups/discover" */'../../groups/pending-requests'); } diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index e354e8422..9bccb68a2 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -800,6 +800,7 @@ "group.tabs.members": "Members", "group.upload_banner": "Upload photo", "groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.", + "groups.discover.popular.show_more": "Show More", "groups.discover.popular.title": "Popular Groups", "groups.discover.search.error.subtitle": "Please try again later.", "groups.discover.search.error.title": "An error occurred", @@ -813,6 +814,7 @@ "groups.discover.search.results.groups": "Groups", "groups.discover.search.results.member_count": "{members, plural, one {member} other {members}}", "groups.discover.suggested.empty": "Unable to fetch suggested groups at this time. Please check back later.", + "groups.discover.suggested.show_more": "Show More", "groups.discover.suggested.title": "Suggested For You", "groups.empty.subtitle": "Start discovering groups to join or create your own.", "groups.empty.title": "No Groups yet", @@ -820,6 +822,7 @@ "groups.pending.empty.subtitle": "You have no pending requests at this time.", "groups.pending.empty.title": "No pending requests", "groups.pending.label": "Pending Requests", + "groups.popular.label": "Suggested Groups", "hashtag.column_header.tag_mode.all": "and {additional}", "hashtag.column_header.tag_mode.any": "or {additional}", "hashtag.column_header.tag_mode.none": "without {additional}", From 7070630eaf274e7eb931e032581bb88d876a52df Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 14 Mar 2023 13:47:24 -0400 Subject: [PATCH 12/12] useVersion -> useBackend --- app/soapbox/hooks/{useVersion.ts => useBackend.ts} | 4 ++-- app/soapbox/hooks/useGroupRoles.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename app/soapbox/hooks/{useVersion.ts => useBackend.ts} (83%) diff --git a/app/soapbox/hooks/useVersion.ts b/app/soapbox/hooks/useBackend.ts similarity index 83% rename from app/soapbox/hooks/useVersion.ts rename to app/soapbox/hooks/useBackend.ts index 2ed75f5da..a511abb2a 100644 --- a/app/soapbox/hooks/useVersion.ts +++ b/app/soapbox/hooks/useBackend.ts @@ -7,10 +7,10 @@ import { useInstance } from './useInstance'; * * @returns Backend */ -const useVersion = () => { +const useBackend = () => { const instance = useInstance(); return parseVersion(instance.version); }; -export { useVersion }; \ No newline at end of file +export { useBackend }; \ No newline at end of file diff --git a/app/soapbox/hooks/useGroupRoles.ts b/app/soapbox/hooks/useGroupRoles.ts index eba96a570..dd435ce16 100644 --- a/app/soapbox/hooks/useGroupRoles.ts +++ b/app/soapbox/hooks/useGroupRoles.ts @@ -1,6 +1,6 @@ import { TRUTHSOCIAL } from 'soapbox/utils/features'; -import { useVersion } from './useVersion'; +import { useBackend } from './useBackend'; enum TruthSocialGroupRoles { ADMIN = 'owner', @@ -26,7 +26,7 @@ const roleMap = { * @returns Object */ const useGroupRoles = () => { - const version = useVersion(); + const version = useBackend(); const isTruthSocial = version.software === TRUTHSOCIAL; const selectedRoles = isTruthSocial ? TruthSocialGroupRoles : BaseGroupRoles;