kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Use entities with Group Members
rodzic
e0437a3b99
commit
92bec6c85e
|
@ -0,0 +1,3 @@
|
|||
export enum Entities {
|
||||
GROUP_MEMBERSHIPS = 'GroupMemberships'
|
||||
}
|
|
@ -118,7 +118,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Stack alignItems='center' space={3} className='mt-10 py-4'>
|
||||
<Stack alignItems='center' space={3} className='mx-auto mt-10 w-5/6 py-4'>
|
||||
<Text
|
||||
size='xl'
|
||||
weight='bold'
|
||||
|
@ -132,7 +132,11 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
<GroupMemberCount group={group} />
|
||||
</HStack>
|
||||
|
||||
<Text theme='muted' dangerouslySetInnerHTML={{ __html: group.note_emojified }} />
|
||||
<Text
|
||||
theme='muted'
|
||||
align='center'
|
||||
dangerouslySetInnerHTML={{ __html: group.note_emojified }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<GroupActionButton group={group} />
|
||||
|
|
|
@ -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 (
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<div className='w-full'>
|
||||
<Account account={member.account} withRelationship={false} />
|
||||
</div>
|
||||
|
||||
<HStack alignItems='center' space={2}>
|
||||
{(isMemberAdmin || isMemberModerator) ? (
|
||||
<span
|
||||
className={
|
||||
clsx('inline-flex items-center rounded px-2 py-1 text-xs font-medium capitalize', {
|
||||
'bg-primary-200 text-primary-500': isMemberAdmin,
|
||||
'bg-gray-200 text-gray-900': isMemberModerator,
|
||||
})
|
||||
}
|
||||
>
|
||||
{member.role}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{menu.length > 0 && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
className='px-2'
|
||||
iconClassName='h-4 w-4'
|
||||
children={null}
|
||||
/>
|
||||
|
||||
<MenuList className='w-56'>
|
||||
{menu.map((menuItem, idx) => {
|
||||
if (typeof menuItem?.text === 'undefined') {
|
||||
return <MenuDivider key={idx} />;
|
||||
} 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 (
|
||||
<Comp key={idx} {...itemProps} className='group'>
|
||||
<HStack space={2} alignItems='center'>
|
||||
{menuItem.icon && (
|
||||
<SvgIcon src={menuItem.icon} className='h-4 w-4 flex-none text-gray-700 group-hover:text-gray-800' />
|
||||
)}
|
||||
|
||||
<div className='truncate'>{menuItem.text}</div>
|
||||
</HStack>
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupMemberListItem;
|
|
@ -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<IGroupMember> = ({ 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 (
|
||||
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||
<div className='w-full'>
|
||||
<Account account={account} withRelationship={false} />
|
||||
</div>
|
||||
{menu.length > 0 && (
|
||||
<Menu>
|
||||
<MenuButton
|
||||
as={IconButton}
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
theme='outlined'
|
||||
className='px-2'
|
||||
iconClassName='h-4 w-4'
|
||||
children={null}
|
||||
/>
|
||||
|
||||
<MenuList className='w-56'>
|
||||
{menu.map((menuItem, idx) => {
|
||||
if (typeof menuItem?.text === 'undefined') {
|
||||
return <MenuDivider key={idx} />;
|
||||
} 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 (
|
||||
<Comp key={idx} {...itemProps} className='group'>
|
||||
<HStack space={3} alignItems='center'>
|
||||
{menuItem.icon && (
|
||||
<SvgIcon src={menuItem.icon} className='h-5 w-5 flex-none text-gray-400 group-hover:text-gray-500' />
|
||||
)}
|
||||
|
||||
<div className='truncate'>{menuItem.text}</div>
|
||||
</HStack>
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
import type { Group } from 'soapbox/types/entities';
|
||||
|
||||
interface IGroupMembers {
|
||||
params: RouteParams
|
||||
params: { id: string }
|
||||
}
|
||||
|
||||
const GroupMembers: React.FC<IGroupMembers> = (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 (
|
||||
<React.Fragment key={role}>
|
||||
<CardHeader className='mt-4'>
|
||||
<CardTitle title={intl.formatMessage(messages[`${role}Subheading`])} />
|
||||
</CardHeader>
|
||||
<ScrollableList
|
||||
scrollKey={`group_${role}s-${groupId}`}
|
||||
hasMore={!!memberships?.next}
|
||||
onLoadMore={handler}
|
||||
isLoading={memberships?.isLoading}
|
||||
showLoading={memberships?.isLoading && !memberships?.items?.count()}
|
||||
placeholderComponent={PlaceholderAccount}
|
||||
placeholderCount={3}
|
||||
itemClassName='pb-4 last:pb-0'
|
||||
>
|
||||
{memberships?.items?.map(accountId => (
|
||||
<GroupMember
|
||||
key={accountId}
|
||||
accountId={accountId}
|
||||
accountRole={role}
|
||||
groupId={groupId}
|
||||
relationship={relationship}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
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)}
|
||||
<ScrollableList
|
||||
scrollKey='group-members'
|
||||
hasMore={hasNextPage}
|
||||
onLoadMore={fetchNextPage}
|
||||
isLoading={isLoading || !group}
|
||||
showLoading={!group || isLoading && members.length === 0}
|
||||
placeholderComponent={PlaceholderAccount}
|
||||
placeholderCount={3}
|
||||
className='divide-y divide-solid divide-gray-300'
|
||||
itemClassName='py-3 last:pb-0'
|
||||
>
|
||||
{members.map((member) => (
|
||||
<GroupMemberListItem
|
||||
group={group as Group}
|
||||
member={member}
|
||||
key={member?.account}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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<GroupMember>(
|
||||
[Entities.GROUP_MEMBERSHIPS, `${groupId}:${role}`],
|
||||
`/api/v1/groups/${groupId}/memberships?role=${role}&limit=1`,
|
||||
{ parser: parseGroupMember },
|
||||
);
|
||||
|
||||
return {
|
||||
...result,
|
||||
groupMembers: entities,
|
||||
};
|
||||
}
|
||||
|
||||
export { useGroupMembers };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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),
|
||||
};
|
||||
};
|
|
@ -11,6 +11,7 @@ export { EmojiRecord, normalizeEmoji } from './emoji';
|
|||
export { EmojiReactionRecord } from './emoji-reaction';
|
||||
export { FilterRecord, normalizeFilter } from './filter';
|
||||
export { GroupRecord, 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';
|
||||
|
|
|
@ -200,7 +200,7 @@ const useGroup = (id: string) => {
|
|||
return groups[0];
|
||||
};
|
||||
|
||||
const queryInfo = useQuery(GroupKeys.group(id), getGroup, {
|
||||
const queryInfo = useQuery<Group>(GroupKeys.group(id), getGroup, {
|
||||
enabled: features.groups && !!id,
|
||||
});
|
||||
|
||||
|
|
|
@ -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<typeof useGroupRoles>['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 };
|
|
@ -13,6 +13,7 @@ import {
|
|||
FieldRecord,
|
||||
FilterRecord,
|
||||
GroupRecord,
|
||||
// GroupMember as GroupMemberRecord,
|
||||
GroupRelationshipRecord,
|
||||
HistoryRecord,
|
||||
InstanceRecord,
|
||||
|
@ -45,6 +46,7 @@ type EmojiReaction = ReturnType<typeof EmojiReactionRecord>;
|
|||
type Field = ReturnType<typeof FieldRecord>;
|
||||
type Filter = ReturnType<typeof FilterRecord>;
|
||||
type Group = ReturnType<typeof GroupRecord>;
|
||||
// type GroupMember = GroupMemberRecord
|
||||
type GroupRelationship = ReturnType<typeof GroupRelationshipRecord>;
|
||||
type History = ReturnType<typeof HistoryRecord>;
|
||||
type Instance = ReturnType<typeof InstanceRecord>;
|
||||
|
@ -90,6 +92,7 @@ export {
|
|||
Field,
|
||||
Filter,
|
||||
Group,
|
||||
// GroupMember,
|
||||
GroupRelationship,
|
||||
History,
|
||||
Instance,
|
||||
|
|
Ładowanie…
Reference in New Issue