Remove Truth Social-specific features

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-04-28 14:50:23 +02:00
parent 5ba66f79ba
commit 0308aec65b
163 changed files with 312 additions and 5471 deletions

View File

@ -3,7 +3,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups';
import { openModal } from 'soapbox/actions/modals';
import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup, usePendingGroups } from 'soapbox/api/hooks';
import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/api/hooks';
import { Button } from 'soapbox/components/ui';
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
@ -33,7 +33,6 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
const joinGroup = useJoinGroup(group);
const leaveGroup = useLeaveGroup(group);
const cancelRequest = useCancelMembershipRequest(group);
const { invalidate: invalidatePendingGroups } = usePendingGroups();
const isRequested = group.relationship?.requested;
const isNonMember = !group.relationship?.member && !isRequested;
@ -44,7 +43,6 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
const onJoinGroup = () => joinGroup.mutate({}, {
onSuccess(entity) {
joinGroup.invalidate();
invalidatePendingGroups();
dispatch(fetchGroupRelationshipsSuccess([entity]));
toast.success(
@ -82,7 +80,6 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
requested: false,
};
dispatch(importEntities([entity], Entities.GROUP_RELATIONSHIPS));
invalidatePendingGroups();
},
});
@ -94,7 +91,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
return (
<Button
theme='secondary'
to={`/group/${group.slug}/manage`}
to={`/group/${group.id}/manage`}
>
<FormattedMessage id='group.manage' defaultMessage='Manage Group' />
</Button>

View File

@ -12,7 +12,7 @@ interface IGroupMemberCount {
const GroupMemberCount = ({ group }: IGroupMemberCount) => {
return (
<Link to={`/group/${group.slug}/members`} className='hover:underline'>
<Link to={`/group/${group.id}/members`} className='hover:underline'>
<Text theme='inherit' tag='span' size='sm' weight='medium' data-testid='group-member-count'>
{shortNumberFormat(group.members_count)}
{' '}

View File

@ -27,7 +27,7 @@ describe('<GroupMemberListItem />', () => {
relationship: buildGroupRelationship(),
});
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(() => {
expect(screen.getByTestId('group-member-list-item')).toHaveTextContent(groupMember.account.display_name);
@ -52,7 +52,7 @@ describe('<GroupMemberListItem />', () => {
});
it('should render the correct badge', async () => {
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(() => {
expect(screen.getByTestId('role-badge')).toHaveTextContent('owner');
@ -73,7 +73,7 @@ describe('<GroupMemberListItem />', () => {
});
it('should render the correct badge', async () => {
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(() => {
expect(screen.getByTestId('role-badge')).toHaveTextContent('admin');
@ -94,7 +94,7 @@ describe('<GroupMemberListItem />', () => {
});
it('should render no correct badge', async () => {
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(() => {
expect(screen.queryAllByTestId('role-badge')).toHaveLength(0);
@ -125,36 +125,19 @@ describe('<GroupMemberListItem />', () => {
});
});
describe('when "canPromoteToAdmin is true', () => {
it('should render dropdown with correct Owner actions', async () => {
const user = userEvent.setup();
it('should render dropdown with correct Owner actions', async () => {
const user = userEvent.setup();
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(async() => {
await user.click(screen.getByTestId('icon-button'));
});
const dropdownMenu = screen.getByTestId('dropdown-menu');
expect(dropdownMenu).toHaveTextContent('Assign admin role');
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
expect(dropdownMenu).toHaveTextContent('Ban from group');
await waitFor(async() => {
await user.click(screen.getByTestId('icon-button'));
});
});
describe('when "canPromoteToAdmin is false', () => {
it('should prevent promoting user to Admin', async () => {
const user = userEvent.setup();
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin={false} />);
await waitFor(async() => {
await user.click(screen.getByTestId('icon-button'));
await user.click(screen.getByTitle('Assign admin role'));
});
expect(screen.getByTestId('toast')).toHaveTextContent('Admin limit reached');
});
const dropdownMenu = screen.getByTestId('dropdown-menu');
expect(dropdownMenu).toHaveTextContent('Assign admin role');
expect(dropdownMenu).toHaveTextContent('Kick @tiger from group');
expect(dropdownMenu).toHaveTextContent('Ban from group');
});
});
@ -180,7 +163,7 @@ describe('<GroupMemberListItem />', () => {
it('should render dropdown with correct Owner actions', async () => {
const user = userEvent.setup();
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(async() => {
await user.click(screen.getByTestId('icon-button'));
@ -219,7 +202,7 @@ describe('<GroupMemberListItem />', () => {
it('should render dropdown with correct Admin actions', async () => {
const user = userEvent.setup();
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(async() => {
await user.click(screen.getByTestId('icon-button'));
@ -252,7 +235,7 @@ describe('<GroupMemberListItem />', () => {
});
it('should not render the dropdown', async () => {
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(async() => {
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
@ -280,7 +263,7 @@ describe('<GroupMemberListItem />', () => {
});
it('should not render the dropdown', async () => {
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(async() => {
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);
@ -310,7 +293,7 @@ describe('<GroupMemberListItem />', () => {
});
it('should not render the dropdown', async () => {
render(<GroupMemberListItem group={group} member={groupMember} canPromoteToAdmin />);
render(<GroupMemberListItem group={group} member={groupMember} />);
await waitFor(async() => {
expect(screen.queryAllByTestId('icon-button')).toHaveLength(0);

View File

@ -11,12 +11,10 @@ import { HStack } from 'soapbox/components/ui';
import { deleteEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account';
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import { useAppDispatch } from 'soapbox/hooks';
import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast';
import { MAX_ADMIN_COUNT } from '../group-members';
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
import type { Group, GroupMember } from 'soapbox/types/entities';
@ -43,14 +41,12 @@ const messages = defineMessages({
interface IGroupMemberListItem {
member: GroupMember;
group: Group;
canPromoteToAdmin: boolean;
}
const GroupMemberListItem = (props: IGroupMemberListItem) => {
const { canPromoteToAdmin, member, group } = props;
const { member, group } = props;
const dispatch = useAppDispatch();
const features = useFeatures();
const intl = useIntl();
const blockGroupMember = useBlockGroupMember(group, member.account);
@ -95,13 +91,6 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
};
const handleAdminAssignment = () => {
if (!canPromoteToAdmin) {
toast.error(intl.formatMessage(messages.adminLimitTitle), {
summary: intl.formatMessage(messages.adminLimitSummary, { count: MAX_ADMIN_COUNT }),
});
return;
}
dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.promoteConfirm),
message: intl.formatMessage(messages.promoteConfirmMessage, { name: account?.username }),
@ -156,13 +145,11 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
(isMemberAdmin || isMemberUser) &&
member.role !== group.relationship.role
) {
if (features.groupsKick) {
items.push({
text: intl.formatMessage(messages.groupModKick, { name: account.username }),
icon: require('@tabler/icons/outline/user-minus.svg'),
action: handleKickFromGroup,
});
}
items.push({
text: intl.formatMessage(messages.groupModKick, { name: account.username }),
icon: require('@tabler/icons/outline/user-minus.svg'),
action: handleKickFromGroup,
});
items.push({
text: intl.formatMessage(messages.groupModBlock, { name: account.username }),

View File

@ -2,29 +2,21 @@ import React, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
import { useLeaveGroup, useMuteGroup, useUnmuteGroup } from 'soapbox/api/hooks';
import { useLeaveGroup } from 'soapbox/api/hooks';
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
import { IconButton } from 'soapbox/components/ui';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import { useAppDispatch } from 'soapbox/hooks';
import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast';
import type { Account, Group } from 'soapbox/types/entities';
import type { Group } from 'soapbox/types/entities';
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?' },
muteConfirm: { id: 'confirmations.mute_group.confirm', defaultMessage: 'Mute' },
muteHeading: { id: 'confirmations.mute_group.heading', defaultMessage: 'Mute Group' },
muteMessage: { id: 'confirmations.mute_group.message', defaultMessage: 'You are about to mute the group. Do you want to continue?' },
muteSuccess: { id: 'group.mute.success', defaultMessage: 'Muted the group' },
unmuteSuccess: { id: 'group.unmute.success', defaultMessage: 'Unmuted the group' },
leave: { id: 'group.leave.label', defaultMessage: 'Leave' },
leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' },
mute: { id: 'group.mute.label', defaultMessage: 'Mute' },
unmute: { id: 'group.unmute.label', defaultMessage: 'Unmute' },
report: { id: 'group.report.label', defaultMessage: 'Report' },
share: { id: 'group.share.label', defaultMessage: 'Share' },
});
@ -34,12 +26,9 @@ interface IGroupActionButton {
}
const GroupOptionsButton = ({ group }: IGroupActionButton) => {
const { account } = useOwnAccount();
const dispatch = useAppDispatch();
const intl = useIntl();
const muteGroup = useMuteGroup(group);
const unmuteGroup = useUnmuteGroup(group);
const leaveGroup = useLeaveGroup(group);
const isMember = group.relationship?.role === GroupRoles.USER;
@ -57,27 +46,6 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
});
};
const handleMute = () =>
dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.muteHeading),
message: intl.formatMessage(messages.muteMessage),
confirm: intl.formatMessage(messages.muteConfirm),
confirmationTheme: 'primary',
onConfirm: () => muteGroup.mutate(undefined, {
onSuccess() {
toast.success(intl.formatMessage(messages.muteSuccess));
},
}),
}));
const handleUnmute = () => {
unmuteGroup.mutate(undefined, {
onSuccess() {
toast.success(intl.formatMessage(messages.unmuteSuccess));
},
});
};
const handleLeave = () =>
dispatch(openModal('CONFIRM', {
heading: intl.formatMessage(messages.confirmationHeading),
@ -103,22 +71,6 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
});
}
if (isInGroup) {
items.push({
text: isMuting ? intl.formatMessage(messages.unmute) : intl.formatMessage(messages.mute),
icon: require('@tabler/icons/outline/volume-3.svg'),
action: isMuting ? handleUnmute : handleMute,
});
}
if (isMember || isAdmin) {
items.push({
text: intl.formatMessage(messages.report),
icon: require('@tabler/icons/outline/flag.svg'),
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
});
}
if (isAdmin) {
items.push(null);
items.push({

View File

@ -1,124 +0,0 @@
import React from 'react';
import { buildGroup, buildGroupTag, buildGroupRelationship } from 'soapbox/jest/factory';
import { render, screen } from 'soapbox/jest/test-helpers';
import { GroupRoles } from 'soapbox/schemas/group-member';
import GroupTagListItem from './group-tag-list-item';
describe('<GroupTagListItem />', () => {
describe('tag name', () => {
const name = 'hello';
it('should render the tag name', () => {
const group = buildGroup();
const tag = buildGroupTag({ name });
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.getByTestId('group-tag-list-item')).toHaveTextContent(`#${name}`);
});
describe('when the tag is "visible"', () => {
const group = buildGroup();
const tag = buildGroupTag({ name, visible: true });
it('renders the default name', () => {
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900');
});
});
describe('when the tag is not "visible" and user is Owner', () => {
const group = buildGroup({
relationship: buildGroupRelationship({
role: GroupRoles.OWNER,
member: true,
}),
});
const tag = buildGroupTag({
name,
visible: false,
});
it('renders the subtle name', () => {
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-400');
});
});
describe('when the tag is not "visible" and user is Admin or User', () => {
const group = buildGroup({
relationship: buildGroupRelationship({
role: GroupRoles.ADMIN,
member: true,
}),
});
const tag = buildGroupTag({
name,
visible: false,
});
it('renders the subtle name', () => {
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.getByTestId('group-tag-name')).toHaveClass('text-gray-900');
});
});
});
describe('pinning', () => {
describe('as an owner', () => {
const group = buildGroup({
relationship: buildGroupRelationship({
role: GroupRoles.OWNER,
member: true,
}),
});
describe('when the tag is visible', () => {
const tag = buildGroupTag({ visible: true });
it('renders the pin icon', () => {
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.getByTestId('pin-icon')).toBeInTheDocument();
});
});
describe('when the tag is not visible', () => {
const tag = buildGroupTag({ visible: false });
it('does not render the pin icon', () => {
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0);
});
});
});
describe('as a non-owner', () => {
const group = buildGroup({
relationship: buildGroupRelationship({
role: GroupRoles.ADMIN,
member: true,
}),
});
describe('when the tag is pinned', () => {
const tag = buildGroupTag({ pinned: true, visible: true });
it('does render the pin icon', () => {
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
screen.debug();
expect(screen.queryAllByTestId('pin-icon')).toHaveLength(1);
});
});
describe('when the tag is not pinned', () => {
const tag = buildGroupTag({ pinned: false, visible: true });
it('does not render the pin icon', () => {
render(<GroupTagListItem group={group} tag={tag} isPinnable />);
expect(screen.queryAllByTestId('pin-icon')).toHaveLength(0);
});
});
});
});
});

View File

@ -1,196 +0,0 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { useUpdateGroupTag } from 'soapbox/api/hooks';
import { HStack, Icon, IconButton, Stack, Text, Tooltip } from 'soapbox/components/ui';
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import { useAppDispatch } from 'soapbox/hooks';
import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import type { Group, GroupTag } from 'soapbox/schemas';
const messages = defineMessages({
hideTag: { id: 'group.tags.hide', defaultMessage: 'Hide topic' },
showTag: { id: 'group.tags.show', defaultMessage: 'Show topic' },
total: { id: 'group.tags.total', defaultMessage: 'Total Posts' },
pinTag: { id: 'group.tags.pin', defaultMessage: 'Pin topic' },
unpinTag: { id: 'group.tags.unpin', defaultMessage: 'Unpin topic' },
pinSuccess: { id: 'group.tags.pin.success', defaultMessage: 'Pinned!' },
unpinSuccess: { id: 'group.tags.unpin.success', defaultMessage: 'Unpinned!' },
visibleSuccess: { id: 'group.tags.visible.success', defaultMessage: 'Topic marked as visible' },
hiddenSuccess: { id: 'group.tags.hidden.success', defaultMessage: 'Topic marked as hidden' },
});
interface IGroupMemberListItem {
tag: GroupTag;
group: Group;
isPinnable: boolean;
}
const GroupTagListItem = (props: IGroupMemberListItem) => {
const { group, tag, isPinnable } = props;
const dispatch = useAppDispatch();
const intl = useIntl();
const { updateGroupTag } = useUpdateGroupTag(group.id, tag.id);
const isOwner = group.relationship?.role === GroupRoles.OWNER;
const toggleVisibility = () => {
const isHiding = tag.visible;
updateGroupTag({
group_tag_type: isHiding ? 'hidden' : 'normal',
}, {
onSuccess() {
const entity: GroupTag = {
...tag,
visible: !tag.visible,
pinned: isHiding ? false : tag.pinned, // unpin if we're hiding
};
dispatch(importEntities([entity], Entities.GROUP_TAGS));
toast.success(
entity.visible ?
intl.formatMessage(messages.visibleSuccess) :
intl.formatMessage(messages.hiddenSuccess),
);
},
});
};
const togglePin = () => {
updateGroupTag({
group_tag_type: tag.pinned ? 'normal' : 'pinned',
}, {
onSuccess() {
const entity = {
...tag,
pinned: !tag.pinned,
};
dispatch(importEntities([entity], Entities.GROUP_TAGS));
toast.success(
entity.pinned ?
intl.formatMessage(messages.pinSuccess) :
intl.formatMessage(messages.unpinSuccess),
);
},
});
};
const renderPinIcon = () => {
if (!isOwner && tag.pinned) {
return (
<Icon
src={require('@tabler/icons/filled/pin.svg')}
className='h-5 w-5 text-gray-600'
data-testid='pin-icon'
/>
);
}
if (!isOwner) {
return null;
}
if (isPinnable) {
return (
<Tooltip
text={
tag.pinned ?
intl.formatMessage(messages.unpinTag) :
intl.formatMessage(messages.pinTag)
}
>
<IconButton
onClick={togglePin}
theme='transparent'
src={
tag.pinned ?
require('@tabler/icons/filled/pin.svg') :
require('@tabler/icons/outline/pin.svg')
}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
data-testid='pin-icon'
/>
</Tooltip>
);
}
if (!isPinnable && tag.pinned) {
return (
<Tooltip text={intl.formatMessage(messages.unpinTag)}>
<IconButton
onClick={togglePin}
theme='transparent'
src={require('@tabler/icons/filled/pin.svg')}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
/>
</Tooltip>
);
}
};
return (
<HStack
alignItems='center'
justifyContent='between'
data-testid='group-tag-list-item'
>
<Link to={`/group/${group.slug}/tag/${tag.id}`} className='group grow'>
<Stack>
<Text
weight='bold'
theme={(tag.visible || !isOwner) ? 'default' : 'subtle'}
className='group-hover:underline'
data-testid='group-tag-name'
>
#{tag.name}
</Text>
<Text size='sm' theme={(tag.visible || !isOwner) ? 'muted' : 'subtle'}>
{intl.formatMessage(messages.total)}:
{' '}
<Text size='sm' theme='inherit' weight='semibold' tag='span'>
{shortNumberFormat(tag.uses)}
</Text>
</Text>
</Stack>
</Link>
<HStack alignItems='center' space={2}>
{tag.visible ? (
renderPinIcon()
) : null}
{isOwner ? (
<Tooltip
text={
tag.visible ?
intl.formatMessage(messages.hideTag) :
intl.formatMessage(messages.showTag)
}
>
<IconButton
onClick={toggleVisibility}
theme='transparent'
src={
tag.visible ?
require('@tabler/icons/outline/eye.svg') :
require('@tabler/icons/outline/eye-off.svg')
}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
/>
</Tooltip>
) : null}
</HStack>
</HStack>
);
};
export default GroupTagListItem;

View File

@ -1,59 +0,0 @@
import React, { useMemo } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { Input, Streamfield } from 'soapbox/components/ui';
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
const messages = defineMessages({
hashtagPlaceholder: { id: 'manage_group.fields.hashtag_placeholder', defaultMessage: 'Add a topic' },
});
interface IGroupTagsField {
tags: string[];
onChange(tags: string[]): void;
onAddItem(): void;
onRemoveItem(i: number): void;
maxItems?: number;
}
const GroupTagsField: React.FC<IGroupTagsField> = ({ tags, onChange, onAddItem, onRemoveItem, maxItems = 3 }) => {
return (
<Streamfield
label={<FormattedMessage id='group.tags.label' defaultMessage='Tags' />}
hint={<FormattedMessage id='group.tags.hint' defaultMessage='Add up to 3 keywords that will serve as core topics of discussion in the group.' />}
component={HashtagField}
values={tags}
onChange={onChange}
onAddItem={onAddItem}
onRemoveItem={onRemoveItem}
maxItems={maxItems}
minItems={1}
/>
);
};
const HashtagField: StreamfieldComponent<string> = ({ value, onChange, autoFocus = false }) => {
const intl = useIntl();
const formattedValue = useMemo(() => {
return `#${value}`;
}, [value]);
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
onChange(target.value.replace('#', ''));
};
return (
<Input
outerClassName='w-full'
type='text'
value={formattedValue}
onChange={handleChange}
placeholder={intl.formatMessage(messages.hashtagPlaceholder)}
autoFocus={autoFocus}
/>
);
};
export default GroupTagsField;

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import React, { useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useGroup, useGroupTags, useUpdateGroup } from 'soapbox/api/hooks';
import { useGroup, useUpdateGroup } from 'soapbox/api/hooks';
import { Button, Column, Form, FormActions, FormGroup, Icon, Input, Spinner, Textarea } from 'soapbox/components/ui';
import { useAppSelector, useInstance } from 'soapbox/hooks';
import { useImageField, useTextField } from 'soapbox/hooks/forms';
@ -11,8 +11,6 @@ import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
import AvatarPicker from '../edit-profile/components/avatar-picker';
import HeaderPicker from '../edit-profile/components/header-picker';
import GroupTagsField from './components/group-tags-field';
const nonDefaultAvatar = (url: string | undefined) => url && isDefaultAvatar(url) ? undefined : url;
const nonDefaultHeader = (url: string | undefined) => url && isDefaultHeader(url) ? undefined : url;
@ -35,10 +33,8 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
const { group, isLoading } = useGroup(groupId);
const { updateGroup } = useUpdateGroup(groupId);
const { invalidate } = useGroupTags(groupId);
const [isSubmitting, setIsSubmitting] = useState(false);
const [tags, setTags] = useState<string[]>(['']);
const avatar = useImageField({ maxPixels: 400 * 400, preview: nonDefaultAvatar(group?.avatar) });
const header = useImageField({ maxPixels: 1920 * 1080, preview: nonDefaultHeader(group?.header) });
@ -61,10 +57,8 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
note: note.value,
avatar: avatar.file === null ? '' : avatar.file,
header: header.file === null ? '' : header.file,
tags,
}, {
onSuccess() {
invalidate();
toast.success(intl.formatMessage(messages.groupSaved));
},
onError(error) {
@ -79,22 +73,6 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
setIsSubmitting(false);
}
const handleAddTag = () => {
setTags([...tags, '']);
};
const handleRemoveTag = (i: number) => {
const newTags = [...tags];
newTags.splice(i, 1);
setTags(newTags);
};
useEffect(() => {
if (group) {
setTags(group.tags.map((t) => t.name));
}
}, [group?.id]);
if (isLoading) {
return <Spinner />;
}
@ -130,15 +108,6 @@ const EditGroup: React.FC<IEditGroup> = ({ params: { groupId } }) => {
/>
</FormGroup>
<div className='pb-6'>
<GroupTagsField
tags={tags}
onChange={setTags}
onAddItem={handleAddTag}
onRemoveItem={handleRemoveTag}
/>
</div>
<FormActions>
<Button theme='primary' type='submit' disabled={isSubmitting} block>
<FormattedMessage id='edit_profile.save' defaultMessage='Save' />

View File

@ -82,7 +82,7 @@ const GroupBlockedMembers: React.FC<IGroupBlockedMembers> = ({ params }) => {
const emptyMessage = <FormattedMessage id='empty_column.group_blocks' defaultMessage="The group hasn't banned any users yet." />;
return (
<Column label={intl.formatMessage(messages.heading)} backHref={`/group/${group.slug}/manage`}>
<Column label={intl.formatMessage(messages.heading)} backHref={`/group/${group.id}/manage`}>
<ScrollableList
scrollKey='group_blocks'
emptyMessage={emptyMessage}

View File

@ -4,7 +4,6 @@ import React, { useMemo } from 'react';
import { useGroup, useGroupMembers, useGroupMembershipRequests } from 'soapbox/api/hooks';
import { PendingItemsRow } from 'soapbox/components/pending-items-row';
import ScrollableList from 'soapbox/components/scrollable-list';
import { useFeatures } from 'soapbox/hooks';
import { GroupRoles } from 'soapbox/schemas/group-member';
import PlaceholderAccount from '../placeholder/components/placeholder-account';
@ -18,13 +17,9 @@ interface IGroupMembers {
params: { groupId: string };
}
export const MAX_ADMIN_COUNT = 5;
const GroupMembers: React.FC<IGroupMembers> = (props) => {
const { groupId } = props.params;
const features = useFeatures();
const { group, isFetching: isFetchingGroup } = useGroup(groupId);
const { groupMembers: owners, isFetching: isFetchingOwners } = useGroupMembers(groupId, GroupRoles.OWNER);
const { groupMembers: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, GroupRoles.ADMIN);
@ -39,10 +34,6 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
...users,
], [owners, admins, users]);
const canPromoteToAdmin = features.groupsAdminMax
? members.filter((member) => member.role === GroupRoles.ADMIN).length < MAX_ADMIN_COUNT
: true;
return (
<>
<ScrollableList
@ -58,7 +49,7 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
prepend={(pendingCount > 0) && (
<div className={clsx('py-3', { 'border-b border-gray-200 dark:border-gray-800': members.length })}>
<PendingItemsRow
to={`/group/${group?.slug}/manage/requests`}
to={`/group/${group?.id}/manage/requests`}
count={pendingCount}
/>
</div>
@ -69,7 +60,6 @@ const GroupMembers: React.FC<IGroupMembers> = (props) => {
group={group as Group}
member={member}
key={member.account.id}
canPromoteToAdmin={canPromoteToAdmin}
/>
))}
</ScrollableList>

View File

@ -1,68 +0,0 @@
import React, { useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { expandGroupTimelineFromTag } from 'soapbox/actions/timelines';
import { useGroup, useGroupTag } from 'soapbox/api/hooks';
import { Column, Icon, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks';
import Timeline from '../ui/components/timeline';
type RouteParams = { tagId: string; groupId: string };
interface IGroupTimeline {
params: RouteParams;
}
const GroupTagTimeline: React.FC<IGroupTimeline> = (props) => {
const dispatch = useAppDispatch();
const groupId = props.params.groupId;
const tagId = props.params.tagId;
const { group } = useGroup(groupId);
const { tag, isLoading } = useGroupTag(tagId);
const handleLoadMore = (maxId: string) => {
dispatch(expandGroupTimelineFromTag(groupId, tag?.name as string, { maxId }));
};
useEffect(() => {
if (tag?.name) {
dispatch(expandGroupTimelineFromTag(groupId, tag?.name));
}
}, [groupId, tag]);
if (isLoading || !tag || !group) {
return null;
}
return (
<Column label={`#${tag.name}`}>
<Timeline
scrollKey='group_timeline'
timelineId={`group:tags:${groupId}:${tag.name}`}
onLoadMore={handleLoadMore}
divideType='border'
showGroup={false}
emptyMessageCard={false}
emptyMessage={
<Stack space={4} className='py-6' justifyContent='center' alignItems='center'>
<div className='rounded-full bg-gray-200 p-4 dark:bg-gray-800'>
<Icon
src={require('@tabler/icons/outline/message-2.svg')}
className='h-6 w-6 text-gray-600'
/>
</div>
<Text theme='muted'>
<FormattedMessage id='empty_column.group' defaultMessage='There are no posts in this group yet.' />
</Text>
</Stack>
}
/>
</Column>
);
};
export default GroupTagTimeline;

View File

@ -1,68 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useGroup, useGroupTags } from 'soapbox/api/hooks';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Icon, Stack, Text } from 'soapbox/components/ui';
import PlaceholderAccount from '../placeholder/components/placeholder-account';
import GroupTagListItem from './components/group-tag-list-item';
import type { Group } from 'soapbox/types/entities';
interface IGroupTopics {
params: { groupId: string };
}
const GroupTopics: React.FC<IGroupTopics> = (props) => {
const { groupId } = props.params;
const { group, isFetching: isFetchingGroup } = useGroup(groupId);
const { tags, isFetching: isFetchingTags, hasNextPage, fetchNextPage } = useGroupTags(groupId);
const isLoading = isFetchingGroup || isFetchingTags;
const pinnedTags = tags.filter((tag) => tag.pinned);
const isPinnable = pinnedTags.length < 3;
return (
<ScrollableList
scrollKey='group-tags'
hasMore={hasNextPage}
onLoadMore={fetchNextPage}
isLoading={isLoading || !group}
showLoading={!group || isLoading && tags.length === 0}
placeholderComponent={PlaceholderAccount}
placeholderCount={3}
listClassName='divide-y divide-solid divide-gray-300 dark:divide-gray-800'
itemClassName='py-3 last:pb-0'
emptyMessage={
<Stack space={4} className='pt-6' justifyContent='center' alignItems='center'>
<div className='rounded-full bg-gray-200 p-4 dark:bg-gray-800'>
<Icon
src={require('@tabler/icons/outline/hash.svg')}
className='h-6 w-6 text-gray-600'
/>
</div>
<Text theme='muted'>
<FormattedMessage id='group.tags.empty' defaultMessage='There are no topics in this group yet.' />
</Text>
</Stack>
}
emptyMessageCard={false}
>
{tags.map((tag) => (
<GroupTagListItem
key={tag.id}
group={group as Group}
isPinnable={isPinnable}
tag={tag}
/>
))}
</ScrollableList>
);
};
export default GroupTopics;

View File

@ -3,10 +3,10 @@ import React, { useEffect, useRef } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { groupCompose, setGroupTimelineVisible, uploadCompose } from 'soapbox/actions/compose';
import { groupCompose, uploadCompose } from 'soapbox/actions/compose';
import { expandGroupFeaturedTimeline, expandGroupTimeline } from 'soapbox/actions/timelines';
import { useGroup, useGroupStream } from 'soapbox/api/hooks';
import { Avatar, HStack, Icon, Stack, Text, Toggle } from 'soapbox/components/ui';
import { Avatar, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
import ComposeForm from 'soapbox/features/compose/components/compose-form';
import { useAppDispatch, useAppSelector, useDraggedFiles, useOwnAccount } from 'soapbox/hooks';
import { makeGetStatusIds } from 'soapbox/selectors';
@ -33,7 +33,6 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
const composeId = `group:${groupId}`;
const canComposeGroupStatus = !!account && group?.relationship?.member;
const groupTimelineVisible = useAppSelector((state) => !!state.compose.get(composeId)?.group_timeline_visible);
const featuredStatusIds = useAppSelector((state) => getStatusIds(state, { type: `group:${group?.id}:pinned` }));
const { isDragging, isDraggedOver } = useDraggedFiles(composer, (files) => {
@ -44,10 +43,6 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
dispatch(expandGroupTimeline(groupId, { maxId }));
};
const handleToggleChange = () => {
dispatch(setGroupTimelineVisible(composeId, !groupTimelineVisible));
};
useGroupStream(groupId);
useEffect(() => {
@ -89,12 +84,6 @@ const GroupTimeline: React.FC<IGroupTimeline> = (props) => {
<FormattedMessage id='compose_group.share_to_followers' defaultMessage='Share with my followers' />
</Text>
</label>
<Toggle
id='group-timeline-visible'
checked={groupTimelineVisible}
onChange={handleToggleChange}
size='sm'
/>
</HStack>
)}
/>

View File

@ -6,10 +6,9 @@ import { openModal } from 'soapbox/actions/modals';
import { useDeleteGroup, useGroup } from 'soapbox/api/hooks';
import List, { ListItem } from 'soapbox/components/list';
import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui';
import { useAppDispatch, useBackend, useGroupsPath } from 'soapbox/hooks';
import { useAppDispatch } from 'soapbox/hooks';
import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast';
import { TRUTHSOCIAL } from 'soapbox/utils/features';
import ColumnForbidden from '../ui/components/column-forbidden';
@ -36,9 +35,7 @@ interface IManageGroup {
const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
const { groupId: id } = params;
const backend = useBackend();
const dispatch = useAppDispatch();
const groupsPath = useGroupsPath();
const history = useHistory();
const intl = useIntl();
@ -70,14 +67,14 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
deleteGroup.mutate(group.id, {
onSuccess() {
toast.success(intl.formatMessage(messages.deleteSuccess));
history.push(groupsPath);
history.push('/groups');
},
});
},
}));
return (
<Column label={intl.formatMessage(messages.heading)} backHref={`/group/${group.slug}`}>
<Column label={intl.formatMessage(messages.heading)} backHref={`/group/${group.id}`}>
<CardBody className='space-y-4'>
{isOwner && (
<>
@ -86,7 +83,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
</CardHeader>
<List>
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/group/${group.slug}/manage/edit`}>
<ListItem label={intl.formatMessage(messages.editGroup)} to={`/group/${group.id}/manage/edit`}>
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
</ListItem>
</List>
@ -98,11 +95,9 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
</CardHeader>
<List>
{backend.software !== TRUTHSOCIAL && (
<ListItem label={intl.formatMessage(messages.pendingRequests)} to={`/group/${group.slug}/manage/requests`} />
)}
<ListItem label={intl.formatMessage(messages.pendingRequests)} to={`/group/${group.id}/manage/requests`} />
<ListItem label={intl.formatMessage(messages.blockedMembers)} to={`/group/${group.slug}/manage/blocks`} />
<ListItem label={intl.formatMessage(messages.blockedMembers)} to={`/group/${group.id}/manage/blocks`} />
</List>
{isOwner && (