Manage group pages

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
environments/review-mastodon-g-0qbqe2/deployments/1797
marcin mikołajczak 2022-12-18 18:03:41 +01:00
rodzic 18b297ad63
commit 6b92d5f3a5
21 zmienionych plików z 478 dodań i 85 usunięć

Wyświetl plik

@ -131,7 +131,7 @@ const createGroup = (params: Record<string, any>, shouldReset?: boolean) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(createGroupRequest());
api(getState).post('/api/v1/groups', params, {
return api(getState).post('/api/v1/groups', params, {
headers: {
'Content-Type': 'multipart/form-data',
},
@ -166,7 +166,7 @@ const updateGroup = (id: string, params: Record<string, any>, shouldReset?: bool
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(updateGroupRequest());
api(getState).put(`/api/v1/groups/${id}`, params)
return api(getState).put(`/api/v1/groups/${id}`, params)
.then(({ data }) => {
dispatch(importFetchedGroups([data]));
dispatch(updateGroupSuccess(data));
@ -196,7 +196,7 @@ const updateGroupFail = (error: AxiosError) => ({
const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(deleteGroupRequest(id));
api(getState).delete(`/api/v1/groups/${id}`)
return api(getState).delete(`/api/v1/groups/${id}`)
.then(() => dispatch(deleteGroupSuccess(id)))
.catch(err => dispatch(deleteGroupFail(id, err)));
};
@ -221,7 +221,7 @@ const fetchGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootS
dispatch(fetchGroupRelationships([id]));
dispatch(fetchGroupRequest(id));
api(getState).get(`/api/v1/groups/${id}`)
return api(getState).get(`/api/v1/groups/${id}`)
.then(({ data }) => {
dispatch(importFetchedGroups([data]));
dispatch(fetchGroupSuccess(data));
@ -248,7 +248,7 @@ const fetchGroupFail = (id: string, error: AxiosError) => ({
const fetchGroups = () => (dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupsRequest());
api(getState).get('/api/v1/groups')
return api(getState).get('/api/v1/groups')
.then(({ data }) => {
dispatch(importFetchedGroups(data));
dispatch(fetchGroupsSuccess(data));
@ -282,7 +282,7 @@ const fetchGroupRelationships = (groupIds: string[]) =>
dispatch(fetchGroupRelationshipsRequest(newGroupIds));
api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
return api(getState).get(`/api/v1/groups/relationships?${newGroupIds.map(id => `id[]=${id}`).join('&')}`).then(response => {
dispatch(fetchGroupRelationshipsSuccess(response.data));
}).catch(error => {
dispatch(fetchGroupRelationshipsFail(error));
@ -314,7 +314,7 @@ const joinGroup = (id: string) =>
dispatch(joinGroupRequest(id, locked));
api(getState).post(`/api/v1/groups/${id}/join`).then(response => {
return api(getState).post(`/api/v1/groups/${id}/join`).then(response => {
dispatch(joinGroupSuccess(response.data));
dispatch(snackbar.success(locked ? messages.joinRequestSuccess : messages.joinSuccess));
}).catch(error => {
@ -326,7 +326,7 @@ const leaveGroup = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(leaveGroupRequest(id));
api(getState).post(`/api/v1/groups/${id}/leave`).then(response => {
return api(getState).post(`/api/v1/groups/${id}/leave`).then(response => {
dispatch(leaveGroupSuccess(response.data));
dispatch(snackbar.success(messages.leaveSuccess));
}).catch(error => {
@ -376,7 +376,7 @@ const groupDeleteStatus = (groupId: string, statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(groupDeleteStatusRequest(groupId, statusId));
api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`)
return api(getState).delete(`/api/v1/groups/${groupId}/statuses/${statusId}`)
.then(() => {
dispatch(deleteFromTimelines(statusId));
dispatch(groupDeleteStatusSuccess(groupId, statusId));
@ -434,7 +434,7 @@ const fetchGroupBlocks = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupBlocksRequest(id));
api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => {
return api(getState).get(`/api/v1/groups/${id}/blocks`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -473,7 +473,7 @@ const expandGroupBlocks = (id: string) =>
dispatch(expandGroupBlocksRequest(id));
api(getState).get(url).then(response => {
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -620,7 +620,7 @@ const fetchGroupMemberships = (id: string, role: GroupRole) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupMembershipsRequest(id, role));
api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => {
return api(getState).get(`/api/v1/groups/${id}/memberships`, { params: { role } }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
@ -662,7 +662,7 @@ const expandGroupMemberships = (id: string, role: GroupRole) =>
dispatch(expandGroupMembershipsRequest(id, role));
api(getState).get(url).then(response => {
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data.map((membership: APIEntity) => membership.account)));
@ -698,7 +698,7 @@ const fetchGroupMembershipRequests = (id: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(fetchGroupMembershipRequestsRequest(id));
api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => {
return api(getState).get(`/api/v1/groups/${id}/membership_requests`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -737,7 +737,7 @@ const expandGroupMembershipRequests = (id: string) =>
dispatch(expandGroupMembershipRequestsRequest(id));
api(getState).get(url).then(response => {
return api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedAccounts(response.data));
@ -770,7 +770,7 @@ const authorizeGroupMembershipRequest = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(authorizeGroupMembershipRequestRequest(groupId, accountId));
api(getState)
return api(getState)
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`)
.then(() => dispatch(authorizeGroupMembershipRequestSuccess(groupId, accountId)))
.catch(error => dispatch(authorizeGroupMembershipRequestFail(groupId, accountId, error)));
@ -799,7 +799,7 @@ const rejectGroupMembershipRequest = (groupId: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(rejectGroupMembershipRequestRequest(groupId, accountId));
api(getState)
return api(getState)
.post(`/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`)
.then(() => dispatch(rejectGroupMembershipRequestSuccess(groupId, accountId)))
.catch(error => dispatch(rejectGroupMembershipRequestFail(groupId, accountId, error)));

Wyświetl plik

@ -1,4 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link, useHistory } from 'react-router-dom';
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
@ -11,6 +12,7 @@ import { displayFqn } from 'soapbox/utils/state';
import RelativeTimestamp from './relative-timestamp';
import { Avatar, Emoji, HStack, Icon, IconButton, Stack, Text } from './ui';
import type { StatusApprovalStatus } from 'soapbox/normalizers/status';
import type { Account as AccountEntity } from 'soapbox/types/entities';
interface IInstanceFavicon {
@ -68,6 +70,7 @@ interface IAccount {
withLinkToProfile?: boolean,
withRelationship?: boolean,
showEdit?: boolean,
approvalStatus?: StatusApprovalStatus,
emoji?: string,
note?: string,
}
@ -92,6 +95,7 @@ const Account = ({
withLinkToProfile = true,
withRelationship = true,
showEdit = false,
approvalStatus,
emoji,
note,
}: IAccount) => {
@ -236,6 +240,18 @@ const Account = ({
</>
) : null}
{approvalStatus && ['pending', 'rejected'].includes(approvalStatus) && (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Text tag='span' theme='muted' size='sm'>
{approvalStatus === 'pending'
? <FormattedMessage id='status.pending' defaultMessage='Pending approval' />
: <FormattedMessage id='status.rejected' defaultMessage='Rejected' />}
</Text>
</>
)}
{showEdit ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>

Wyświetl plik

@ -388,6 +388,7 @@ const Status: React.FC<IStatus> = (props) => {
showEdit={!!actualStatus.edited_at}
showProfileHoverCard={hoverable}
withLinkToProfile={hoverable}
approvalStatus={actualStatus.approval_status}
/>
</div>

Wyświetl plik

@ -0,0 +1,24 @@
import React, { useCallback } from 'react';
import GroupCard from 'soapbox/components/group-card';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetGroup } from 'soapbox/selectors';
interface IGroupContainer {
id: string
}
const GroupContainer: React.FC<IGroupContainer> = (props) => {
const { id, ...rest } = props;
const getGroup = useCallback(makeGetGroup(), []);
const group = useAppSelector(state => getGroup(state, id));
if (group) {
return <GroupCard group={group} {...rest} />;
} else {
return null;
}
};
export default GroupContainer;

Wyświetl plik

@ -9,8 +9,10 @@ import IconButton from 'soapbox/components/icon-button';
import ScrollableList from 'soapbox/components/scrollable-list';
import { HStack, Tabs, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import GroupContainer from 'soapbox/containers/group-container';
import StatusContainer from 'soapbox/containers/status-container';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account';
import PlaceholderGroupCard from 'soapbox/features/placeholder/components/placeholder-group-card';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder-hashtag';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
@ -179,23 +181,15 @@ const SearchResults = () => {
if (selectedFilter === 'groups') {
hasMore = results.groupsHasMore;
loaded = results.groupsLoaded;
placeholderComponent = PlaceholderGroupCard;
if (results.groups && results.groups.size > 0) {
searchResults = results.groups.map((groupId: string) => (
<></>
<GroupContainer id={groupId} />
));
resultsIds = results.groups;
} else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) {
// searchResults = trendingStatuses.map((statusId: string) => (
// // @ts-ignore
// <StatusContainer
// key={statusId}
// id={statusId}
// onMoveUp={handleMoveUp}
// onMoveDown={handleMoveDown}
// />
// ));
// resultsIds = trendingStatuses;
searchResults = null;
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>

Wyświetl plik

@ -3,10 +3,10 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import { getSettings } from 'soapbox/actions/settings';
import Account from 'soapbox/components/account';
import Badge from 'soapbox/components/badge';
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
import { Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import ActionButton from 'soapbox/features/ui/components/action-button';
import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
@ -51,8 +51,8 @@ const AccountCard: React.FC<IAccountCard> = ({ id }) => {
</div>
<Stack space={4} className='p-3'>
<AccountContainer
id={account.id}
<Account
account={account}
withRelationship={false}
/>

Wyświetl plik

@ -2,7 +2,7 @@ import { List as ImmutableList } from 'immutable';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { editGroup, joinGroup, leaveGroup } from 'soapbox/actions/groups';
import { joinGroup, leaveGroup } from 'soapbox/actions/groups';
import { openModal } from 'soapbox/actions/modals';
import StillImage from 'soapbox/components/still-image';
import { Avatar, Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
@ -47,24 +47,15 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
);
}
const onJoinGroup = () => {
dispatch(joinGroup(group.id));
};
const onJoinGroup = () => dispatch(joinGroup(group.id));
const onLeaveGroup = () => {
const onLeaveGroup = () =>
dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.confirmationHeading),
message: intl.formatMessage(messages.confirmationMessage),
confirm: intl.formatMessage(messages.confirmationConfirm),
onConfirm: () => {
dispatch(leaveGroup(group.id));
},
onConfirm: () => dispatch(leaveGroup(group.id)),
}));
};
const onEditGroup = () => {
dispatch(editGroup(group));
};
const onAvatarClick = () => {
const avatar = normalizeAttachment({
@ -131,13 +122,24 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
);
}
if (group.relationship.requested) {
return (
<Button
theme='secondary'
onClick={onLeaveGroup}
>
<FormattedMessage id='group.cancel_request' defaultMessage='Cancel request' />
</Button>
);
}
if (group.relationship?.role === 'admin') {
return (
<Button
theme='secondary'
onClick={onEditGroup}
to={`/groups/${group.id}/manage`}
>
<FormattedMessage id='group.manage' defaultMessage='Edit group' />
<FormattedMessage id='group.manage' defaultMessage='Manage group' />
</Button>
);
}

Wyświetl plik

@ -0,0 +1,104 @@
import React, { useCallback, useEffect } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { fetchGroup, fetchGroupBlocks, groupUnblock } from 'soapbox/actions/groups';
import snackbar from 'soapbox/actions/snackbar';
import Account from 'soapbox/components/account';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Button, Column, HStack, Spinner } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetAccount, makeGetGroup } from 'soapbox/selectors';
import ColumnForbidden from '../ui/components/column-forbidden';
type RouteParams = { id: string };
const messages = defineMessages({
heading: { id: 'column.group_blocked_members', defaultMessage: 'Blocked members' },
unblock: { id: 'group.group_mod_unblock', defaultMessage: 'Unblock' },
unblocked: { id: 'group.group_mod_unblock.success', defaultMessage: 'Unblocked @{name} from group' },
});
interface IBlockedMember {
accountId: string
groupId: string
}
const BlockedMember: React.FC<IBlockedMember> = ({ accountId, groupId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const getAccount = useCallback(makeGetAccount(), []);
const account = useAppSelector((state) => getAccount(state, accountId));
if (!account) return null;
const handleUnblock = () =>
dispatch(groupUnblock(groupId, accountId)).then(() => {
dispatch(snackbar.success(intl.formatMessage(messages.unblocked, { name: account.acct })));
});
return (
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
<div className='w-full'>
<Account account={account} withRelationship={false} />
</div>
<Button
theme='danger'
size='sm'
text={intl.formatMessage(messages.unblock)}
onClick={handleUnblock}
/>
</HStack>
);
};
interface IGroupBlockedMembers {
params: RouteParams
}
const GroupBlockedMembers: React.FC<IGroupBlockedMembers> = ({ params }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const id = params?.id || '';
const getGroup = useCallback(makeGetGroup(), []);
const group = useAppSelector(state => getGroup(state, id));
const accountIds = useAppSelector((state) => state.user_lists.group_blocks.get(id)?.items);
useEffect(() => {
if (!group) dispatch(fetchGroup(id));
dispatch(fetchGroupBlocks(id));
}, [id]);
if (!group || !group.relationship || !accountIds) {
return (
<Column label={intl.formatMessage(messages.heading)}>
<Spinner />
</Column>
);
}
if (!group.relationship.role || !['admin', 'moderator'].includes(group.relationship.role)) {
return (<ColumnForbidden />);
}
const emptyMessage = <FormattedMessage id='empty_column.group_blocks' defaultMessage="The group hasn't blocked any users yet." />;
return (
<Column label={intl.formatMessage(messages.heading)} backHref={`/groups/${id}/manage`}>
<ScrollableList
scrollKey='group_blocks'
emptyMessage={emptyMessage}
>
{accountIds.map((accountId) =>
<BlockedMember key={accountId} accountId={accountId} groupId={id} />,
)}
</ScrollableList>
</Column>
);
};
export default GroupBlockedMembers;

Wyświetl plik

@ -6,10 +6,10 @@ import { Link } from 'react-router-dom';
import { expandGroupMemberships, fetchGroup, fetchGroupMemberships, groupBlock, groupDemoteAccount, groupKick, groupPromoteAccount } from 'soapbox/actions/groups';
import { openModal } from 'soapbox/actions/modals';
import snackbar from 'soapbox/actions/snackbar';
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 AccountContainer from 'soapbox/containers/account-container';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
@ -161,7 +161,7 @@ const GroupMember: React.FC<IGroupMember> = ({ accountId, accountRole, groupId,
return (
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
<div className='w-full'>
<AccountContainer id={accountId} withRelationship={false} />
<Account account={account} withRelationship={false} />
</div>
{menu.length > 0 && (
<Menu>

Wyświetl plik

@ -0,0 +1,119 @@
import React, { useCallback, useEffect } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { authorizeGroupMembershipRequest, fetchGroup, fetchGroupMembershipRequests, rejectGroupMembershipRequest } from 'soapbox/actions/groups';
import snackbar from 'soapbox/actions/snackbar';
import Account from 'soapbox/components/account';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Button, Column, HStack, Spinner } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetAccount, makeGetGroup } from 'soapbox/selectors';
import ColumnForbidden from '../ui/components/column-forbidden';
type RouteParams = { id: string };
const messages = defineMessages({
heading: { id: 'column.group_pending_requests', defaultMessage: 'Pending requests' },
authorize: { id: 'group.group_mod_authorize', defaultMessage: 'Accept' },
authorized: { id: 'group.group_mod_authorize.success', defaultMessage: 'Accepted @{name} to group' },
reject: { id: 'group.group_mod_reject', defaultMessage: 'Reject' },
rejected: { id: 'group.group_mod_reject.success', defaultMessage: 'Rejected @{name} from group' },
});
interface IMembershipRequest {
accountId: string
groupId: string
}
const MembershipRequest: React.FC<IMembershipRequest> = ({ accountId, groupId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const getAccount = useCallback(makeGetAccount(), []);
const account = useAppSelector((state) => getAccount(state, accountId));
if (!account) return null;
const handleAuthorize = () =>
dispatch(authorizeGroupMembershipRequest(groupId, accountId)).then(() => {
dispatch(snackbar.success(intl.formatMessage(messages.authorized, { name: account.acct })));
});
const handleReject = () =>
dispatch(rejectGroupMembershipRequest(groupId, accountId)).then(() => {
dispatch(snackbar.success(intl.formatMessage(messages.rejected, { name: account.acct })));
});
return (
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
<div className='w-full'>
<Account account={account} withRelationship={false} />
</div>
<HStack space={2}>
<Button
theme='secondary'
size='sm'
text={intl.formatMessage(messages.authorize)}
onClick={handleAuthorize}
/>
<Button
theme='danger'
size='sm'
text={intl.formatMessage(messages.reject)}
onClick={handleReject}
/>
</HStack>
</HStack>
);
};
interface IGroupMembershipRequests {
params: RouteParams
}
const GroupMembershipRequests: React.FC<IGroupMembershipRequests> = ({ params }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const id = params?.id || '';
const getGroup = useCallback(makeGetGroup(), []);
const group = useAppSelector(state => getGroup(state, id));
const accountIds = useAppSelector((state) => state.user_lists.membership_requests.get(id)?.items);
useEffect(() => {
if (!group) dispatch(fetchGroup(id));
dispatch(fetchGroupMembershipRequests(id));
}, [id]);
if (!group || !group.relationship || !accountIds) {
return (
<Column label={intl.formatMessage(messages.heading)}>
<Spinner />
</Column>
);
}
if (!group.relationship.role || !['admin', 'moderator'].includes(group.relationship.role)) {
return (<ColumnForbidden />);
}
const emptyMessage = <FormattedMessage id='empty_column.group_membership_requests' defaultMessage='There are no pending membership requests for this group.' />;
return (
<Column label={intl.formatMessage(messages.heading)} backHref={`/groups/${id}/manage`}>
<ScrollableList
scrollKey='group_membership_requests'
emptyMessage={emptyMessage}
>
{accountIds.map((accountId) =>
<MembershipRequest key={accountId} accountId={accountId} groupId={id} />,
)}
</ScrollableList>
</Column>
);
};
export default GroupMembershipRequests;

Wyświetl plik

@ -0,0 +1,96 @@
import React, { useCallback, useEffect } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { deleteGroup, editGroup, fetchGroup } from 'soapbox/actions/groups';
import { openModal } from 'soapbox/actions/modals';
import List, { ListItem } from 'soapbox/components/list';
import { CardBody, Column, Spinner } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetGroup } from 'soapbox/selectors';
import ColumnForbidden from '../ui/components/column-forbidden';
type RouteParams = { id: string };
const messages = defineMessages({
heading: { id: 'column.manage_group', defaultMessage: 'Manage group' },
editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' },
pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending requests' },
blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Blocked members' },
deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' },
deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' },
deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' },
deleteMessage: { id: 'confirmations.delete_group.message', defaultMessage: 'Are you sure you want to delete this group? This is a permanent action that cannot be undone.' },
});
interface IManageGroup {
params: RouteParams
}
const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
const history = useHistory();
const intl = useIntl();
const dispatch = useAppDispatch();
const id = params?.id || '';
const getGroup = useCallback(makeGetGroup(), []);
const group = useAppSelector(state => getGroup(state, id));
useEffect(() => {
if (!group) dispatch(fetchGroup(id));
}, [id]);
if (!group || !group.relationship) {
return (
<Column label={intl.formatMessage(messages.heading)}>
<Spinner />
</Column>
);
}
if (!group.relationship.role || !['admin', 'moderator'].includes(group.relationship.role)) {
return (<ColumnForbidden />);
}
const onEditGroup = () =>
dispatch(editGroup(group));
const onDeleteGroup = () =>
dispatch(openModal('CONFIRM', {
icon: require('@tabler/icons/trash.svg'),
heading: intl.formatMessage(messages.deleteHeading),
message: intl.formatMessage(messages.deleteMessage),
confirm: intl.formatMessage(messages.deleteConfirm),
onConfirm: () => dispatch(deleteGroup(id)),
}));
const navigateToPending = () => history.push(`/groups/${id}/manage/requests`);
const navigateToBlocks = () => history.push(`/groups/${id}/manage/blocks`);
return (
<Column label={intl.formatMessage(messages.heading)} backHref={`/groups/${id}`}>
<CardBody className='space-y-4'>
{group.relationship.role === 'admin' && (
<List>
<ListItem label={intl.formatMessage(messages.editGroup)} onClick={onEditGroup}>
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
</ListItem>
</List>
)}
<List>
<ListItem label={intl.formatMessage(messages.pendingRequests)} onClick={navigateToPending} />
<ListItem label={intl.formatMessage(messages.blockedMembers)} onClick={navigateToBlocks} />
</List>
{group.relationship.role === 'admin' && (
<List>
<ListItem label={intl.formatMessage(messages.deleteGroup)} onClick={onDeleteGroup} />
</List>
)}
</CardBody>
</Column>
);
};
export default ManageGroup;

Wyświetl plik

@ -5,9 +5,10 @@ import { Link } from 'react-router-dom';
import { createSelector } from 'reselect';
import { fetchGroups } from 'soapbox/actions/groups';
import { openModal } from 'soapbox/actions/modals';
import GroupCard from 'soapbox/components/group-card';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Column, Spinner, Stack, Text } from 'soapbox/components/ui';
import { Button, Column, Spinner, Stack, Text } from 'soapbox/components/ui';
import { useAppSelector } from 'soapbox/hooks';
import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card';
@ -37,6 +38,10 @@ const Groups: React.FC = () => {
dispatch(fetchGroups());
}, []);
const createGroup = () => {
dispatch(openModal('MANAGE_GROUP'));
};
if (!groups) {
return (
<Column>
@ -66,21 +71,32 @@ const Groups: React.FC = () => {
);
return (
<ScrollableList
scrollKey='groups'
emptyMessage={emptyMessage}
itemClassName='py-3 last:pb-0'
isLoading={isLoading}
showLoading={isLoading && !groups.count()}
placeholderComponent={PlaceholderGroupCard}
placeholderCount={3}
>
{groups.map((group) => (
<Link key={group.id} to={`/groups/${group.id}`}>
<GroupCard group={group as GroupEntity} />
</Link>
))}
</ScrollableList>
<Stack className='gap-4'>
<Button
className='sm:w-fit sm:self-end xl:hidden'
icon={require('@tabler/icons/circles.svg')}
onClick={createGroup}
theme='secondary'
block
>
<FormattedMessage id='new_group_panel.action' defaultMessage='Create group' />
</Button>
<ScrollableList
scrollKey='groups'
emptyMessage={emptyMessage}
itemClassName='py-3 first:pt-0 last:pb-0'
isLoading={isLoading}
showLoading={isLoading && !groups.count()}
placeholderComponent={PlaceholderGroupCard}
placeholderCount={3}
>
{groups.map((group) => (
<Link key={group.id} to={`/groups/${group.id}`}>
<GroupCard group={group as GroupEntity} />
</Link>
))}
</ScrollableList>
</Stack>
);
};

Wyświetl plik

@ -8,16 +8,16 @@ import { reblog, favourite, unreblog, unfavourite } from 'soapbox/actions/intera
import { openModal } from 'soapbox/actions/modals';
import { getSettings } from 'soapbox/actions/settings';
import { hideStatus, revealStatus } from 'soapbox/actions/statuses';
import Account from 'soapbox/components/account';
import Icon from 'soapbox/components/icon';
import { HStack, Text, Emoji } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import StatusContainer from 'soapbox/containers/status-container';
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
import { makeGetNotification } from 'soapbox/selectors';
import { NotificationType, validType } from 'soapbox/utils/notification';
import type { ScrollPosition } from 'soapbox/components/status';
import type { Account, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities';
import type { Account as AccountEntity, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities';
const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: Date) => {
const output = [message];
@ -27,7 +27,7 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp
return output.join(', ');
};
const buildLink = (account: Account): JSX.Element => (
const buildLink = (account: AccountEntity): JSX.Element => (
<bdi>
<Link
className='text-gray-800 dark:text-gray-200 font-bold hover:underline'
@ -127,7 +127,7 @@ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
const buildMessage = (
intl: IntlShape,
type: NotificationType,
account: Account,
account: AccountEntity,
totalCount: number | null,
targetName: string,
instanceTitle: string,
@ -287,16 +287,16 @@ const Notification: React.FC<INotificaton> = (props) => {
case 'follow':
case 'user_approved':
return account && typeof account === 'object' ? (
<AccountContainer
id={account.id}
<Account
account={account}
hidden={hidden}
avatarSize={48}
/>
) : null;
case 'follow_request':
return account && typeof account === 'object' ? (
<AccountContainer
id={account.id}
<Account
account={account}
hidden={hidden}
avatarSize={48}
actionType='follow_request'
@ -304,8 +304,8 @@ const Notification: React.FC<INotificaton> = (props) => {
) : null;
case 'move':
return account && typeof account === 'object' && notification.target && typeof notification.target === 'object' ? (
<AccountContainer
id={notification.target.id}
<Account
account={notification.target}
hidden={hidden}
avatarSize={48}
/>

Wyświetl plik

@ -1,11 +1,11 @@
import classNames from 'clsx';
import React from 'react';
import Account from 'soapbox/components/account';
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
import StatusContent from 'soapbox/components/status-content';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import { HStack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import PollPreview from 'soapbox/features/ui/components/poll-preview';
import { useAppSelector } from 'soapbox/hooks';
@ -31,9 +31,9 @@ const ScheduledStatus: React.FC<IScheduledStatus> = ({ statusId, ...other }) =>
<div className={classNames('status', `status-${status.visibility}`, { 'status-reply': !!status.in_reply_to_id })} data-id={status.id}>
<div className='mb-4'>
<HStack justifyContent='between' alignItems='start'>
<AccountContainer
<Account
key={account.id}
id={account.id}
account={account}
timestamp={status.created_at}
futureTimestamp
/>

Wyświetl plik

@ -1,6 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { FormattedDate, FormattedMessage, useIntl } from 'react-intl';
import Account from 'soapbox/components/account';
import Icon from 'soapbox/components/icon';
import StatusContent from 'soapbox/components/status-content';
import StatusMedia from 'soapbox/components/status-media';
@ -8,7 +9,6 @@ import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay';
import TranslateButton from 'soapbox/components/translate-button';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
import { getActualStatus } from 'soapbox/utils/status';
@ -84,12 +84,13 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
<div className='border-box'>
<div ref={node} className='detailed-actualStatus' tabIndex={-1}>
<div className='mb-4'>
<AccountContainer
<Account
key={account.id}
id={account.id}
account={account}
timestamp={actualStatus.created_at}
avatarSize={42}
hideActions
approvalStatus={actualStatus.approval_status}
/>
</div>

Wyświetl plik

@ -74,7 +74,7 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
return (
<Modal
title={id
? <FormattedMessage id='navigation_bar.manage_group' defaultMessage='Manage Group' />
? <FormattedMessage id='navigation_bar.edit_group' defaultMessage='Edit Group' />
: <FormattedMessage id='navigation_bar.create_group' defaultMessage='Create Group' />}
confirmationAction={handleNextStep}
confirmationText={confirmationText}

Wyświetl plik

@ -1,10 +1,10 @@
import classNames from 'clsx';
import React from 'react';
import Account from 'soapbox/components/account';
import StatusContent from 'soapbox/components/status-content';
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
import { Card, HStack } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card';
import PlaceholderMediaGallery from 'soapbox/features/placeholder/components/placeholder-media-gallery';
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
@ -65,9 +65,9 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
>
<div className='mb-4'>
<HStack justifyContent='between' alignItems='start'>
<AccountContainer
<Account
key={account.id}
id={account.id}
account={account}
timestamp={status.created_at}
hideActions
/>

Wyświetl plik

@ -116,6 +116,9 @@ import {
Groups,
GroupMembers,
GroupTimeline,
ManageGroup,
GroupBlockedMembers,
GroupMembershipRequests,
} from './util/async-components';
import { WrappedRoute } from './util/react-router-helpers';
@ -280,6 +283,9 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
{features.groups && <WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/members' exact page={GroupPage} component={GroupMembers} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/manage' exact page={DefaultPage} component={ManageGroup} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/manage/blocks' exact page={DefaultPage} component={GroupBlockedMembers} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/manage/requests' exact page={DefaultPage} component={GroupMembershipRequests} content={children} />}
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
<WrappedRoute path='/statuses/:statusId' exact page={StatusPage} component={Status} content={children} />

Wyświetl plik

@ -554,6 +554,18 @@ export function GroupTimeline() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline');
}
export function ManageGroup() {
return import(/* webpackChunkName: "features/groups" */'../../group/manage-group');
}
export function GroupBlockedMembers() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-blocked-members');
}
export function GroupMembershipRequests() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-membership-requests');
}
export function ManageGroupModal() {
return import(/* webpackChunkName: "features/manage_group_modal" */'../components/modals/manage-group-modal/manage-group-modal');
}

Wyświetl plik

@ -910,7 +910,7 @@
"navigation_bar.in_reply_to": "W odpowiedzi do",
"navigation_bar.invites": "Zaproszenia",
"navigation_bar.logout": "Wyloguj",
"navigation_bar.manage_group": "Zarządzaj grupą",
"navigation_bar.edit_group": "Edytuj grupę",
"navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.preferences": "Preferencje",
"navigation_bar.profile_directory": "Katalog profilów",

Wyświetl plik

@ -19,6 +19,7 @@ import { normalizePoll } from 'soapbox/normalizers/poll';
import type { ReducerAccount } from 'soapbox/reducers/accounts';
import type { Account, Attachment, Card, Emoji, Group, Mention, Poll, EmbeddedEntity } from 'soapbox/types/entities';
export type StatusApprovalStatus = 'pending' | 'approval' | 'rejected';
export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'self';
export type EventJoinMode = 'free' | 'restricted' | 'invite';
@ -40,6 +41,7 @@ export const EventRecord = ImmutableRecord({
export const StatusRecord = ImmutableRecord({
account: null as EmbeddedEntity<Account | ReducerAccount>,
application: null as ImmutableMap<string, any> | null,
approval_status: 'approved' as StatusApprovalStatus,
bookmarked: false,
card: null as Card | null,
content: '',