Groups: simple member list
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
0256c34406
commit
0d617d4de8
5 changed files with 100 additions and 7 deletions
89
app/soapbox/features/group/group-members.tsx
Normal file
89
app/soapbox/features/group/group-members.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { expandGroupMemberships, fetchGroup, fetchGroupMemberships } from 'soapbox/actions/groups';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { CardHeader, CardTitle } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { List } from 'soapbox/reducers/group-memberships';
|
||||
|
||||
type RouteParams = { id: string };
|
||||
|
||||
interface IGroupMembers {
|
||||
params: RouteParams,
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.group_members', defaultMessage: 'Group members' },
|
||||
adminSubheading: { id: 'groups.admin_subheading', defaultMessage: 'Group administrators' },
|
||||
moderatorSubheading: { id: 'groups.moderator_subheading', defaultMessage: 'Group moderators' },
|
||||
userSubheading: { id: 'groups.user_subheading', defaultMessage: 'Users' },
|
||||
});
|
||||
|
||||
const GroupMembers: React.FC<IGroupMembers> = (props) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const groupId = props.params.id;
|
||||
|
||||
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 handleLoadMore = (role: 'admin' | 'moderator' | 'user') => {
|
||||
dispatch(expandGroupMemberships(groupId, role));
|
||||
};
|
||||
|
||||
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: 'admin' | 'moderator' | 'user', 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}
|
||||
>
|
||||
{memberships?.items?.map(accountId => <AccountContainer key={accountId} id={accountId} withRelationship={false} />)}
|
||||
</ScrollableList>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchGroup(groupId));
|
||||
|
||||
dispatch(fetchGroupMemberships(groupId, 'admin'));
|
||||
dispatch(fetchGroupMemberships(groupId, 'moderator'));
|
||||
dispatch(fetchGroupMemberships(groupId, 'user'));
|
||||
}, [groupId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderMemberships(admins, 'admin', handleLoadMoreAdmins)}
|
||||
{renderMemberships(moderators, 'moderator', handleLoadMoreModerators)}
|
||||
{renderMemberships(users, 'user', handleLoadMoreUsers)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupMembers;
|
|
@ -32,16 +32,14 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
|||
dispatch(fetchGroup(groupId));
|
||||
dispatch(expandGroupTimeline(groupId));
|
||||
|
||||
dispatch(groupCompose(`group:${groupId}`, groupId));
|
||||
|
||||
const disconnect = dispatch(connectGroupStream(groupId));
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(groupCompose(`group:${groupId}`, groupId));
|
||||
}, []);
|
||||
}, [groupId]);
|
||||
|
||||
return (
|
||||
<Stack space={2}>
|
||||
|
|
|
@ -114,6 +114,7 @@ import {
|
|||
EventDiscussion,
|
||||
Events,
|
||||
Groups,
|
||||
GroupMembers,
|
||||
GroupTimeline,
|
||||
} from './util/async-components';
|
||||
import { WrappedRoute } from './util/react-router-helpers';
|
||||
|
@ -278,6 +279,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
|
||||
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} />
|
||||
<WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />
|
||||
<WrappedRoute path='/groups/:id/members' exact page={GroupPage} component={GroupMembers} content={children} />
|
||||
|
||||
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
|
||||
<WrappedRoute path='/statuses/:statusId' exact page={StatusPage} component={Status} content={children} />
|
||||
|
|
|
@ -546,6 +546,10 @@ export function Groups() {
|
|||
return import(/* webpackChunkName: "features/groups" */'../../groups');
|
||||
}
|
||||
|
||||
export function GroupMembers() {
|
||||
return import(/* webpackChunkName: "features/groups" */'../../group/group-members');
|
||||
}
|
||||
|
||||
export function GroupTimeline() {
|
||||
return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline');
|
||||
}
|
||||
|
|
|
@ -30,11 +30,11 @@ const ReducerRecord = ImmutableRecord({
|
|||
});
|
||||
|
||||
export type GroupRole = 'admin' | 'moderator' | 'user';
|
||||
type List = ReturnType<typeof ListRecord>;
|
||||
export type List = ReturnType<typeof ListRecord>;
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
const normalizeList = (state: State, path: string[], memberships: APIEntity[], next: string | null) => {
|
||||
return state.setIn(path, ImmutableMap({
|
||||
return state.setIn(path, ListRecord({
|
||||
next,
|
||||
items: ImmutableOrderedSet(memberships.map(item => item.account.id)),
|
||||
isLoading: false,
|
||||
|
|
Loading…
Reference in a new issue