Merge remote-tracking branch 'origin/develop' into group-modal
This commit is contained in:
commit
8cca3c4e6e
|
@ -165,7 +165,7 @@ const AuthorizeRejectButton: React.FC<IAuthorizeRejectButton> = ({ theme, icon,
|
||||||
src={isLoading ? require('@tabler/icons/player-stop-filled.svg') : icon}
|
src={isLoading ? require('@tabler/icons/player-stop-filled.svg') : icon}
|
||||||
onClick={action}
|
onClick={action}
|
||||||
theme='seamless'
|
theme='seamless'
|
||||||
className='h-10 w-10 items-center justify-center bg-gray-900'
|
className='h-10 w-10 items-center justify-center bg-white focus:!ring-0 dark:!bg-gray-900'
|
||||||
iconClassName={clsx('h-6 w-6', {
|
iconClassName={clsx('h-6 w-6', {
|
||||||
'text-primary-500': theme === 'primary',
|
'text-primary-500': theme === 'primary',
|
||||||
'text-danger-600': theme === 'danger',
|
'text-danger-600': theme === 'danger',
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { mentionCompose, replyCompose } from 'soapbox/actions/compose';
|
||||||
import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
import { toggleFavourite, toggleReblog } from 'soapbox/actions/interactions';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { toggleStatusHidden, unfilterStatus } from 'soapbox/actions/statuses';
|
import { toggleStatusHidden, unfilterStatus } from 'soapbox/actions/statuses';
|
||||||
import Icon from 'soapbox/components/icon';
|
|
||||||
import TranslateButton from 'soapbox/components/translate-button';
|
import TranslateButton from 'soapbox/components/translate-button';
|
||||||
import AccountContainer from 'soapbox/containers/account-container';
|
import AccountContainer from 'soapbox/containers/account-container';
|
||||||
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
|
import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container';
|
||||||
|
@ -22,7 +21,7 @@ import StatusMedia from './status-media';
|
||||||
import StatusReplyMentions from './status-reply-mentions';
|
import StatusReplyMentions from './status-reply-mentions';
|
||||||
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
import SensitiveContentOverlay from './statuses/sensitive-content-overlay';
|
||||||
import StatusInfo from './statuses/status-info';
|
import StatusInfo from './statuses/status-info';
|
||||||
import { Card, Stack, Text } from './ui';
|
import { Card, Icon, Stack, Text } from './ui';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Account as AccountEntity,
|
Account as AccountEntity,
|
||||||
|
@ -217,7 +216,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
<StatusInfo
|
<StatusInfo
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
to={`/@${status.getIn(['account', 'acct'])}`}
|
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||||
icon={<Icon src={require('@tabler/icons/repeat.svg')} className='text-green-600' />}
|
icon={<Icon src={require('@tabler/icons/repeat.svg')} className='h-4 w-4 text-green-600' />}
|
||||||
text={
|
text={
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='status.reblogged_by'
|
id='status.reblogged_by'
|
||||||
|
@ -242,7 +241,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
return (
|
return (
|
||||||
<StatusInfo
|
<StatusInfo
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
icon={<Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />}
|
icon={<Icon src={require('@tabler/icons/pinned.svg')} className='h-4 w-4 text-gray-600 dark:text-gray-400' />}
|
||||||
text={
|
text={
|
||||||
<Text size='xs' theme='muted' weight='medium'>
|
<Text size='xs' theme='muted' weight='medium'>
|
||||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||||
|
@ -255,7 +254,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
<StatusInfo
|
<StatusInfo
|
||||||
avatarSize={avatarSize}
|
avatarSize={avatarSize}
|
||||||
to={`/groups/${group.id}`}
|
to={`/groups/${group.id}`}
|
||||||
icon={<Icon src={require('@tabler/icons/circles.svg')} className='text-gray-600 dark:text-gray-400' />}
|
icon={<Icon src={require('@tabler/icons/circles.svg')} className='h-4 w-4 text-primary-600 dark:text-accent-blue' />}
|
||||||
text={
|
text={
|
||||||
<Text size='xs' theme='muted' weight='medium'>
|
<Text size='xs' theme='muted' weight='medium'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
|
|
@ -11,24 +11,22 @@ interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
src: string
|
src: string
|
||||||
/** Text to display next ot the button. */
|
/** Text to display next ot the button. */
|
||||||
text?: string
|
text?: string
|
||||||
/** Don't render a background behind the icon. */
|
|
||||||
transparent?: boolean
|
|
||||||
/** Predefined styles to display for the button. */
|
/** Predefined styles to display for the button. */
|
||||||
theme?: 'seamless' | 'outlined' | 'secondary'
|
theme?: 'seamless' | 'outlined' | 'secondary' | 'transparent'
|
||||||
/** Override the data-testid */
|
/** Override the data-testid */
|
||||||
'data-testid'?: string
|
'data-testid'?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A clickable icon. */
|
/** A clickable icon. */
|
||||||
const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => {
|
const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => {
|
||||||
const { src, className, iconClassName, text, transparent = false, theme = 'seamless', ...filteredProps } = props;
|
const { src, className, iconClassName, text, theme = 'seamless', ...filteredProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
type='button'
|
type='button'
|
||||||
className={clsx('flex items-center space-x-2 rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0', {
|
className={clsx('flex items-center space-x-2 rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0', {
|
||||||
'bg-white dark:bg-transparent': !transparent,
|
'bg-white dark:bg-transparent': theme === 'seamless',
|
||||||
'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined',
|
'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined',
|
||||||
'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200': theme === 'secondary',
|
'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200': theme === 'secondary',
|
||||||
'opacity-50': filteredProps.disabled,
|
'opacity-50': filteredProps.disabled,
|
||||||
|
|
|
@ -20,7 +20,6 @@ const Tag: React.FC<ITag> = ({ tag, onDelete }) => {
|
||||||
iconClassName='h-4 w-4'
|
iconClassName='h-4 w-4'
|
||||||
src={require('@tabler/icons/x.svg')}
|
src={require('@tabler/icons/x.svg')}
|
||||||
onClick={() => onDelete(tag)}
|
onClick={() => onDelete(tag)}
|
||||||
transparent
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,4 +3,5 @@ export enum Entities {
|
||||||
GROUPS = 'Groups',
|
GROUPS = 'Groups',
|
||||||
GROUP_RELATIONSHIPS = 'GroupRelationships',
|
GROUP_RELATIONSHIPS = 'GroupRelationships',
|
||||||
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
||||||
|
RELATIONSHIPS = 'Relationships'
|
||||||
}
|
}
|
|
@ -21,7 +21,7 @@ function useEntity<TEntity extends Entity>(
|
||||||
entityFn: EntityFn<void>,
|
entityFn: EntityFn<void>,
|
||||||
opts: UseEntityOpts<TEntity> = {},
|
opts: UseEntityOpts<TEntity> = {},
|
||||||
) {
|
) {
|
||||||
const [isFetching, setPromise] = useLoading();
|
const [isFetching, setPromise] = useLoading(true);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [entityType, entityId] = path;
|
const [entityType, entityId] = path;
|
||||||
|
|
|
@ -40,10 +40,11 @@ describe('<GroupOptionsButton />', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render null', () => {
|
it('should render one option for leaving the group', () => {
|
||||||
render(<GroupOptionsButton group={group} />);
|
render(<GroupOptionsButton group={group} />);
|
||||||
|
|
||||||
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(0);
|
// Leave group option only
|
||||||
|
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,14 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { Button } from 'soapbox/components/ui';
|
import { Button } from 'soapbox/components/ui';
|
||||||
import { deleteEntities } from 'soapbox/entity-store/actions';
|
import { importEntities } from 'soapbox/entity-store/actions';
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useAppDispatch } from 'soapbox/hooks';
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api';
|
import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api';
|
||||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
import type { Group } from 'soapbox/types/entities';
|
import type { Group, GroupRelationship } from 'soapbox/types/entities';
|
||||||
|
|
||||||
interface IGroupActionButton {
|
interface IGroupActionButton {
|
||||||
group: Group
|
group: Group
|
||||||
|
@ -36,6 +36,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
||||||
const isRequested = group.relationship?.requested;
|
const isRequested = group.relationship?.requested;
|
||||||
const isNonMember = !group.relationship?.member && !isRequested;
|
const isNonMember = !group.relationship?.member && !isRequested;
|
||||||
const isOwner = group.relationship?.role === GroupRoles.OWNER;
|
const isOwner = group.relationship?.role === GroupRoles.OWNER;
|
||||||
|
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
|
||||||
const isBlocked = group.relationship?.blocked_by;
|
const isBlocked = group.relationship?.blocked_by;
|
||||||
|
|
||||||
const onJoinGroup = () => joinGroup.mutate({}, {
|
const onJoinGroup = () => joinGroup.mutate({}, {
|
||||||
|
@ -65,7 +66,11 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
||||||
|
|
||||||
const onCancelRequest = () => cancelRequest.mutate({}, {
|
const onCancelRequest = () => cancelRequest.mutate({}, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
dispatch(deleteEntities([group.id], Entities.GROUP_RELATIONSHIPS));
|
const entity = {
|
||||||
|
...group.relationship as GroupRelationship,
|
||||||
|
requested: false,
|
||||||
|
};
|
||||||
|
dispatch(importEntities([entity], Entities.GROUP_RELATIONSHIPS));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOwner) {
|
if (isOwner || isAdmin) {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
theme='secondary'
|
theme='secondary'
|
||||||
|
|
|
@ -9,13 +9,14 @@ import DropdownMenu from 'soapbox/components/dropdown-menu/dropdown-menu';
|
||||||
import { HStack } from 'soapbox/components/ui';
|
import { HStack } from 'soapbox/components/ui';
|
||||||
import { deleteEntities } from 'soapbox/entity-store/actions';
|
import { deleteEntities } from 'soapbox/entity-store/actions';
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useAccount, useAppDispatch, useFeatures } from 'soapbox/hooks';
|
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account';
|
||||||
import { useBlockGroupMember, useDemoteGroupMember, usePromoteGroupMember } from 'soapbox/hooks/api';
|
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||||
|
import { useAccount, useBlockGroupMember, useDemoteGroupMember, usePromoteGroupMember } from 'soapbox/hooks/api';
|
||||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
|
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
|
||||||
import type { Account as AccountEntity, Group, GroupMember } from 'soapbox/types/entities';
|
import type { Group, GroupMember } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban' },
|
blockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban' },
|
||||||
|
@ -51,7 +52,7 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
||||||
const promoteGroupMember = usePromoteGroupMember(group, member);
|
const promoteGroupMember = usePromoteGroupMember(group, member);
|
||||||
const demoteGroupMember = useDemoteGroupMember(group, member);
|
const demoteGroupMember = useDemoteGroupMember(group, member);
|
||||||
|
|
||||||
const account = useAccount(member.account.id) as AccountEntity;
|
const { account, isLoading } = useAccount(member.account.id);
|
||||||
|
|
||||||
// Current user role
|
// Current user role
|
||||||
const isCurrentUserOwner = group.relationship?.role === GroupRoles.OWNER;
|
const isCurrentUserOwner = group.relationship?.role === GroupRoles.OWNER;
|
||||||
|
@ -64,10 +65,10 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
||||||
|
|
||||||
const handleKickFromGroup = () => {
|
const handleKickFromGroup = () => {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
message: intl.formatMessage(messages.kickFromGroupMessage, { name: account.username }),
|
message: intl.formatMessage(messages.kickFromGroupMessage, { name: account?.username }),
|
||||||
confirm: intl.formatMessage(messages.kickConfirm),
|
confirm: intl.formatMessage(messages.kickConfirm),
|
||||||
onConfirm: () => dispatch(groupKick(group.id, account.id)).then(() =>
|
onConfirm: () => dispatch(groupKick(group.id, account?.id as string)).then(() =>
|
||||||
toast.success(intl.formatMessage(messages.kicked, { name: account.acct })),
|
toast.success(intl.formatMessage(messages.kicked, { name: account?.acct })),
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -75,13 +76,13 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
||||||
const handleBlockFromGroup = () => {
|
const handleBlockFromGroup = () => {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
heading: intl.formatMessage(messages.blockFromGroupHeading),
|
heading: intl.formatMessage(messages.blockFromGroupHeading),
|
||||||
message: intl.formatMessage(messages.blockFromGroupMessage, { name: account.username }),
|
message: intl.formatMessage(messages.blockFromGroupMessage, { name: account?.username }),
|
||||||
confirm: intl.formatMessage(messages.blockConfirm),
|
confirm: intl.formatMessage(messages.blockConfirm),
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
blockGroupMember({ account_ids: [member.account.id] }, {
|
blockGroupMember({ account_ids: [member.account.id] }, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
dispatch(deleteEntities([member.id], Entities.GROUP_MEMBERSHIPS));
|
dispatch(deleteEntities([member.id], Entities.GROUP_MEMBERSHIPS));
|
||||||
toast.success(intl.formatMessage(messages.blocked, { name: account.acct }));
|
toast.success(intl.formatMessage(messages.blocked, { name: account?.acct }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -91,14 +92,14 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
||||||
const handleAdminAssignment = () => {
|
const handleAdminAssignment = () => {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
heading: intl.formatMessage(messages.promoteConfirm),
|
heading: intl.formatMessage(messages.promoteConfirm),
|
||||||
message: intl.formatMessage(messages.promoteConfirmMessage, { name: account.username }),
|
message: intl.formatMessage(messages.promoteConfirmMessage, { name: account?.username }),
|
||||||
confirm: intl.formatMessage(messages.promoteConfirm),
|
confirm: intl.formatMessage(messages.promoteConfirm),
|
||||||
confirmationTheme: 'primary',
|
confirmationTheme: 'primary',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
promoteGroupMember({ role: GroupRoles.ADMIN, account_ids: [account.id] }, {
|
promoteGroupMember({ role: GroupRoles.ADMIN, account_ids: [account?.id] }, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success(
|
toast.success(
|
||||||
intl.formatMessage(messages.promotedToAdmin, { name: account.acct }),
|
intl.formatMessage(messages.promotedToAdmin, { name: account?.acct }),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -107,9 +108,9 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUserAssignment = () => {
|
const handleUserAssignment = () => {
|
||||||
demoteGroupMember({ role: GroupRoles.USER, account_ids: [account.id] }, {
|
demoteGroupMember({ role: GroupRoles.USER, account_ids: [account?.id] }, {
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success(intl.formatMessage(messages.demotedToUser, { name: account.acct }));
|
toast.success(intl.formatMessage(messages.demotedToUser, { name: account?.acct }));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -160,7 +161,11 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [group, account]);
|
}, [group, account?.id]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <PlaceholderAccount />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HStack alignItems='center' justifyContent='between'>
|
<HStack alignItems='center' justifyContent='between'>
|
||||||
|
|
|
@ -1,15 +1,23 @@
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
|
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
|
||||||
import { IconButton } from 'soapbox/components/ui';
|
import { IconButton } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||||
|
import { useLeaveGroup } from 'soapbox/hooks/api';
|
||||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
|
import toast from 'soapbox/toast';
|
||||||
|
|
||||||
import type { Account, Group } from 'soapbox/types/entities';
|
import type { Account, Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' },
|
||||||
|
confirmationHeading: { id: 'confirmations.leave_group.heading', defaultMessage: 'Leave group' },
|
||||||
|
confirmationMessage: { id: 'confirmations.leave_group.message', defaultMessage: 'You are about to leave the group. Do you want to continue?' },
|
||||||
|
leave: { id: 'group.leave.label', defaultMessage: 'Leave' },
|
||||||
|
leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' },
|
||||||
report: { id: 'group.report.label', defaultMessage: 'Report' },
|
report: { id: 'group.report.label', defaultMessage: 'Report' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,19 +29,48 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const leaveGroup = useLeaveGroup(group);
|
||||||
|
|
||||||
const isMember = group.relationship?.role === GroupRoles.USER;
|
const isMember = group.relationship?.role === GroupRoles.USER;
|
||||||
|
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
|
||||||
const isBlocked = group.relationship?.blocked_by;
|
const isBlocked = group.relationship?.blocked_by;
|
||||||
|
|
||||||
const menu: Menu = useMemo(() => ([
|
const onLeaveGroup = () =>
|
||||||
{
|
dispatch(openModal('CONFIRM', {
|
||||||
|
heading: intl.formatMessage(messages.confirmationHeading),
|
||||||
|
message: intl.formatMessage(messages.confirmationMessage),
|
||||||
|
confirm: intl.formatMessage(messages.confirmationConfirm),
|
||||||
|
onConfirm: () => leaveGroup.mutate(group.relationship?.id as string, {
|
||||||
|
onSuccess() {
|
||||||
|
leaveGroup.invalidate();
|
||||||
|
toast.success(intl.formatMessage(messages.leaveSuccess));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const menu: Menu = useMemo(() => {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
if (isMember) {
|
||||||
|
items.push({
|
||||||
text: intl.formatMessage(messages.report),
|
text: intl.formatMessage(messages.report),
|
||||||
icon: require('@tabler/icons/flag.svg'),
|
icon: require('@tabler/icons/flag.svg'),
|
||||||
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
|
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
|
||||||
},
|
});
|
||||||
]), []);
|
}
|
||||||
|
|
||||||
if (isBlocked || !isMember || menu.length === 0) {
|
if (isAdmin) {
|
||||||
|
items.push({
|
||||||
|
text: intl.formatMessage(messages.leave),
|
||||||
|
icon: require('@tabler/icons/logout.svg'),
|
||||||
|
action: onLeaveGroup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [isMember, isAdmin]);
|
||||||
|
|
||||||
|
if (isBlocked || menu.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,7 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
|
||||||
onLoadMore={handleLoadMore}
|
onLoadMore={handleLoadMore}
|
||||||
emptyMessage={
|
emptyMessage={
|
||||||
<Stack space={4} className='py-6' justifyContent='center' alignItems='center'>
|
<Stack space={4} className='py-6' justifyContent='center' alignItems='center'>
|
||||||
<div className='rounded-full bg-gray-200 p-4'>
|
<div className='rounded-full bg-gray-200 p-4 dark:bg-gray-800'>
|
||||||
<Icon
|
<Icon
|
||||||
src={require('@tabler/icons/message-2.svg')}
|
src={require('@tabler/icons/message-2.svg')}
|
||||||
className='h-6 w-6 text-gray-600'
|
className='h-6 w-6 text-gray-600'
|
||||||
|
|
|
@ -7,8 +7,10 @@ import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui';
|
import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useGroupsPath } from 'soapbox/hooks';
|
import { useAppDispatch, useGroupsPath } from 'soapbox/hooks';
|
||||||
import { useDeleteGroup, useGroup } from 'soapbox/hooks/api';
|
import { useDeleteGroup, useGroup } from 'soapbox/hooks/api';
|
||||||
|
import { useBackend } from 'soapbox/hooks/useBackend';
|
||||||
import { GroupRoles } from 'soapbox/schemas/group-member';
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
import toast from 'soapbox/toast';
|
import toast from 'soapbox/toast';
|
||||||
|
import { TRUTHSOCIAL } from 'soapbox/utils/features';
|
||||||
|
|
||||||
import ColumnForbidden from '../ui/components/column-forbidden';
|
import ColumnForbidden from '../ui/components/column-forbidden';
|
||||||
|
|
||||||
|
@ -17,8 +19,8 @@ type RouteParams = { id: string };
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.manage_group', defaultMessage: 'Manage group' },
|
heading: { id: 'column.manage_group', defaultMessage: 'Manage group' },
|
||||||
editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' },
|
editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' },
|
||||||
pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending requests' },
|
pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending Requests' },
|
||||||
blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Banned members' },
|
blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Banned Members' },
|
||||||
deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' },
|
deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' },
|
||||||
deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' },
|
||||||
deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' },
|
deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' },
|
||||||
|
@ -34,10 +36,12 @@ interface IManageGroup {
|
||||||
|
|
||||||
const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
const intl = useIntl();
|
|
||||||
const history = useHistory();
|
const backend = useBackend();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const groupsPath = useGroupsPath();
|
const groupsPath = useGroupsPath();
|
||||||
|
const history = useHistory();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const { group } = useGroup(id);
|
const { group } = useGroup(id);
|
||||||
|
|
||||||
|
@ -99,7 +103,10 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<List>
|
<List>
|
||||||
|
{backend.software !== TRUTHSOCIAL && (
|
||||||
<ListItem label={intl.formatMessage(messages.pendingRequests)} onClick={navigateToPending} />
|
<ListItem label={intl.formatMessage(messages.pendingRequests)} onClick={navigateToPending} />
|
||||||
|
)}
|
||||||
|
|
||||||
<ListItem label={intl.formatMessage(messages.blockedMembers)} onClick={navigateToBlocks} />
|
<ListItem label={intl.formatMessage(messages.blockedMembers)} onClick={navigateToBlocks} />
|
||||||
</List>
|
</List>
|
||||||
|
|
||||||
|
|
|
@ -18,4 +18,4 @@ const PlaceholderAccount: React.FC = () => (
|
||||||
</HStack>
|
</HStack>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default PlaceholderAccount;
|
export default React.memo(PlaceholderAccount);
|
||||||
|
|
|
@ -21,4 +21,4 @@ const PlaceholderDisplayName: React.FC<IPlaceholderDisplayName> = ({ minLength,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PlaceholderDisplayName;
|
export default React.memo(PlaceholderDisplayName);
|
||||||
|
|
|
@ -147,7 +147,6 @@ const Header = () => {
|
||||||
src={require('@tabler/icons/help.svg')}
|
src={require('@tabler/icons/help.svg')}
|
||||||
className='cursor-pointer bg-transparent text-gray-700 hover:text-gray-800 dark:text-gray-600 dark:hover:text-gray-500'
|
className='cursor-pointer bg-transparent text-gray-700 hover:text-gray-800 dark:text-gray-600 dark:hover:text-gray-500'
|
||||||
iconClassName='h-5 w-5'
|
iconClassName='h-5 w-5'
|
||||||
transparent
|
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -45,7 +45,7 @@ const ThemeSelector: React.FC<IThemeSelector> = ({ value, onChange }) => {
|
||||||
<Select
|
<Select
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
className='pl-10'
|
className='!pl-10'
|
||||||
>
|
>
|
||||||
<option value='system'>{intl.formatMessage(messages.system)}</option>
|
<option value='system'>{intl.formatMessage(messages.system)}</option>
|
||||||
<option value='light'>{intl.formatMessage(messages.light)}</option>
|
<option value='light'>{intl.formatMessage(messages.light)}</option>
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||||
import { useOwnAccount } from 'soapbox/hooks';
|
import { useApi, useOwnAccount } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { Group, GroupRelationship } from 'soapbox/schemas';
|
import type { Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
function useCancelMembershipRequest(group: Group) {
|
function useCancelMembershipRequest(group: Group) {
|
||||||
|
const api = useApi();
|
||||||
const me = useOwnAccount();
|
const me = useOwnAccount();
|
||||||
|
|
||||||
const { createEntity, isSubmitting } = useEntityActions<GroupRelationship>(
|
const { createEntity, isSubmitting } = useCreateEntity(
|
||||||
[Entities.GROUP_RELATIONSHIPS, group.id],
|
[Entities.GROUP_RELATIONSHIPS],
|
||||||
{ post: `/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject` },
|
() => api.post(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* Accounts
|
||||||
|
*/
|
||||||
|
export { useAccount } from './useAccount';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups
|
* Groups
|
||||||
*/
|
*/
|
||||||
|
@ -13,3 +18,8 @@ export { useJoinGroup } from './groups/useJoinGroup';
|
||||||
export { useLeaveGroup } from './groups/useLeaveGroup';
|
export { useLeaveGroup } from './groups/useLeaveGroup';
|
||||||
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
||||||
export { useUpdateGroup } from './groups/useUpdateGroup';
|
export { useUpdateGroup } from './groups/useUpdateGroup';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationships
|
||||||
|
*/
|
||||||
|
export { useRelationships } from './useRelationships';
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { type Account, accountSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
|
||||||
|
import { useRelationships } from './useRelationships';
|
||||||
|
|
||||||
|
function useAccount(id: string) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entity: account, ...result } = useEntity<Account>(
|
||||||
|
[Entities.ACCOUNTS, id],
|
||||||
|
() => api.get(`/api/v1/accounts/${id}`),
|
||||||
|
{ schema: accountSchema },
|
||||||
|
);
|
||||||
|
const { relationships, isLoading } = useRelationships([account?.id as string]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
isLoading: result.isLoading || isLoading,
|
||||||
|
account: account ? { ...account, relationship: relationships[0] || null } : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useAccount };
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useEntities } from 'soapbox/entity-store/hooks';
|
||||||
|
import { type Relationship, relationshipSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
|
||||||
|
function useRelationships(ids: string[]) {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { entities: relationships, ...result } = useEntities<Relationship>(
|
||||||
|
[Entities.RELATIONSHIPS],
|
||||||
|
() => api.get(`/api/v1/accounts/relationships?${ids.map(id => `id[]=${id}`).join('&')}`),
|
||||||
|
{ schema: relationshipSchema, enabled: ids.filter(Boolean).length > 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
relationships,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useRelationships };
|
|
@ -1,7 +1,7 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
function useLoading() {
|
function useLoading(initialState: boolean = false) {
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
const [isLoading, setIsLoading] = useState<boolean>(initialState);
|
||||||
|
|
||||||
function setPromise<T>(promise: Promise<T>) {
|
function setPromise<T>(promise: Promise<T>) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
|
@ -786,6 +786,7 @@
|
||||||
"group.join.request_success": "Request sent to group owner",
|
"group.join.request_success": "Request sent to group owner",
|
||||||
"group.join.success": "Group joined successfully!",
|
"group.join.success": "Group joined successfully!",
|
||||||
"group.leave": "Leave Group",
|
"group.leave": "Leave Group",
|
||||||
|
"group.leave.label": "Leave",
|
||||||
"group.leave.success": "Left the group",
|
"group.leave.success": "Left the group",
|
||||||
"group.manage": "Manage Group",
|
"group.manage": "Manage Group",
|
||||||
"group.popover.action": "View Group",
|
"group.popover.action": "View Group",
|
||||||
|
@ -933,7 +934,7 @@
|
||||||
"login_external.errors.instance_fail": "The instance returned an error.",
|
"login_external.errors.instance_fail": "The instance returned an error.",
|
||||||
"login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?",
|
"login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?",
|
||||||
"login_form.header": "Sign In",
|
"login_form.header": "Sign In",
|
||||||
"manage_group.blocked_members": "Banned members",
|
"manage_group.blocked_members": "Banned Members",
|
||||||
"manage_group.confirmation.copy": "Copy link",
|
"manage_group.confirmation.copy": "Copy link",
|
||||||
"manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.",
|
"manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.",
|
||||||
"manage_group.confirmation.info_2": "Post the group's first post and get the conversation started.",
|
"manage_group.confirmation.info_2": "Post the group's first post and get the conversation started.",
|
||||||
|
@ -953,7 +954,7 @@
|
||||||
"manage_group.fields.name_placeholder": "Group Name",
|
"manage_group.fields.name_placeholder": "Group Name",
|
||||||
"manage_group.get_started": "Let’s get started!",
|
"manage_group.get_started": "Let’s get started!",
|
||||||
"manage_group.next": "Next",
|
"manage_group.next": "Next",
|
||||||
"manage_group.pending_requests": "Pending requests",
|
"manage_group.pending_requests": "Pending Requests",
|
||||||
"manage_group.privacy.hint": "These settings cannot be changed later.",
|
"manage_group.privacy.hint": "These settings cannot be changed later.",
|
||||||
"manage_group.privacy.label": "Privacy settings",
|
"manage_group.privacy.label": "Privacy settings",
|
||||||
"manage_group.privacy.private.hint": "Discoverable. Users can join after their request is approved.",
|
"manage_group.privacy.private.hint": "Discoverable. Users can join after their request is approved.",
|
||||||
|
|
Loading…
Reference in New Issue