Support Group mutes

environments/review-group-mute-aqtn93/deployments/3537
Chewbacca 2023-06-08 09:00:49 -04:00
rodzic e58dada03a
commit e3fa58c0da
20 zmienionych plików z 373 dodań i 174 usunięć

Wyświetl plik

@ -1,95 +1,12 @@
import { isLoggedIn } from 'soapbox/utils/auth';
import { getNextLinkName } from 'soapbox/utils/quirks';
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
import { importFetchedAccounts } from './importer';
import { openModal } from './modals';
import type { AxiosError } from 'axios';
import type { Account as AccountEntity } from 'soapbox/schemas';
import type { AppDispatch, RootState } from 'soapbox/store';
import type { APIEntity } from 'soapbox/types/entities';
const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL';
const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
import type { AppDispatch } from 'soapbox/store';
import type { Account as AccountEntity } from 'soapbox/types/entities';
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
const fetchMutes = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const nextLinkName = getNextLinkName(getState);
dispatch(fetchMutesRequest());
api(getState).get('/api/v1/mutes').then(response => {
const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
dispatch(importFetchedAccounts(response.data));
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => dispatch(fetchMutesFail(error)));
};
const fetchMutesRequest = () => ({
type: MUTES_FETCH_REQUEST,
});
const fetchMutesSuccess = (accounts: APIEntity[], next: string | null) => ({
type: MUTES_FETCH_SUCCESS,
accounts,
next,
});
const fetchMutesFail = (error: AxiosError) => ({
type: MUTES_FETCH_FAIL,
error,
});
const expandMutes = () =>
(dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return;
const nextLinkName = getNextLinkName(getState);
const url = getState().user_lists.mutes.next;
if (url === null) {
return;
}
dispatch(expandMutesRequest());
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === nextLinkName);
dispatch(importFetchedAccounts(response.data));
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map((item: APIEntity) => item.id)));
}).catch(error => dispatch(expandMutesFail(error)));
};
const expandMutesRequest = () => ({
type: MUTES_EXPAND_REQUEST,
});
const expandMutesSuccess = (accounts: APIEntity[], next: string | null) => ({
type: MUTES_EXPAND_SUCCESS,
accounts,
next,
});
const expandMutesFail = (error: AxiosError) => ({
type: MUTES_EXPAND_FAIL,
error,
});
const initMuteModal = (account: AccountEntity) =>
(dispatch: AppDispatch) => {
dispatch({
@ -114,23 +31,9 @@ const changeMuteDuration = (duration: number) =>
};
export {
MUTES_FETCH_REQUEST,
MUTES_FETCH_SUCCESS,
MUTES_FETCH_FAIL,
MUTES_EXPAND_REQUEST,
MUTES_EXPAND_SUCCESS,
MUTES_EXPAND_FAIL,
MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
MUTES_CHANGE_DURATION,
fetchMutes,
fetchMutesRequest,
fetchMutesSuccess,
fetchMutesFail,
expandMutes,
expandMutesRequest,
expandMutesSuccess,
expandMutesFail,
initMuteModal,
toggleHideNotifications,
changeMuteDuration,

Wyświetl plik

@ -11,7 +11,11 @@ function useGroup(groupId: string, refetch = true) {
const { entity: group, ...result } = useEntity<Group>(
[Entities.GROUPS, groupId],
() => api.get(`/api/v1/groups/${groupId}`),
{ schema: groupSchema, refetch },
{
schema: groupSchema,
refetch,
enabled: !!groupId,
},
);
const { groupRelationship: relationship } = useGroupRelationship(groupId);

Wyświetl plik

@ -0,0 +1,25 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useEntities } from 'soapbox/entity-store/hooks';
import { useFeatures } from 'soapbox/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { groupSchema } from 'soapbox/schemas';
import type { Group } from 'soapbox/schemas';
function useGroupMutes() {
const api = useApi();
const features = useFeatures();
const { entities, ...result } = useEntities<Group>(
[Entities.GROUP_MUTES],
() => api.get('/api/v1/groups/mutes'),
{ schema: groupSchema, enabled: features.groupsMuting },
);
return {
...result,
mutes: entities,
};
}
export { useGroupMutes };

Wyświetl plik

@ -0,0 +1,18 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useEntityActions } from 'soapbox/entity-store/hooks';
import { type Group, groupRelationshipSchema } from 'soapbox/schemas';
function useMuteGroup(group?: Group) {
const { createEntity, isSubmitting } = useEntityActions(
[Entities.GROUP_RELATIONSHIPS, group?.id as string],
{ post: `/api/v1/groups/${group?.id}/mute` },
{ schema: groupRelationshipSchema },
);
return {
mutate: createEntity,
isSubmitting,
};
}
export { useMuteGroup };

Wyświetl plik

@ -0,0 +1,18 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useEntityActions } from 'soapbox/entity-store/hooks';
import { type Group, groupRelationshipSchema } from 'soapbox/schemas';
function useUnmuteGroup(group?: Group) {
const { createEntity, isSubmitting } = useEntityActions(
[Entities.GROUP_RELATIONSHIPS, group?.id as string],
{ post: `/api/v1/groups/${group?.id}/unmute` },
{ schema: groupRelationshipSchema },
);
return {
mutate: createEntity,
isSubmitting,
};
}
export { useUnmuteGroup };

Wyświetl plik

@ -23,6 +23,7 @@ export { useGroupLookup } from './groups/useGroupLookup';
export { useGroupMedia } from './groups/useGroupMedia';
export { useGroupMembers } from './groups/useGroupMembers';
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
export { useGroupMutes } from './groups/useGroupMutes';
export { useGroupRelationship } from './groups/useGroupRelationship';
export { useGroupRelationships } from './groups/useGroupRelationships';
export { useGroupSearch } from './groups/useGroupSearch';
@ -32,10 +33,12 @@ export { useGroupValidation } from './groups/useGroupValidation';
export { useGroups } from './groups/useGroups';
export { useGroupsFromTag } from './groups/useGroupsFromTag';
export { useJoinGroup } from './groups/useJoinGroup';
export { useMuteGroup } from './groups/useMuteGroup';
export { useLeaveGroup } from './groups/useLeaveGroup';
export { usePopularGroups } from './groups/usePopularGroups';
export { usePopularTags } from './groups/usePopularTags';
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
export { useSuggestedGroups } from './groups/useSuggestedGroups';
export { useUnmuteGroup } from './groups/useUnmuteGroup';
export { useUpdateGroup } from './groups/useUpdateGroup';
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';

Wyświetl plik

@ -14,7 +14,7 @@ import { initMuteModal } from 'soapbox/actions/mutes';
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
import { deleteFromTimelines } from 'soapbox/actions/timelines';
import { useGroupRelationship } from 'soapbox/api/hooks';
import { useGroup, useGroupRelationship, useMuteGroup, useUnmuteGroup } from 'soapbox/api/hooks';
import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus';
import DropdownMenu from 'soapbox/components/dropdown-menu';
import StatusActionButton from 'soapbox/components/status-action-button';
@ -65,12 +65,16 @@ const messages = defineMessages({
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
more: { id: 'status.more', defaultMessage: 'More' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
muteConfirm: { id: 'confirmations.mute_group.confirm', defaultMessage: 'Mute' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
muteGroup: { id: 'group.mute.long_label', defaultMessage: 'Mute Group' },
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' },
open: { id: 'status.open', defaultMessage: 'Expand this post' },
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
pinToGroup: { id: 'status.pin_to_group', defaultMessage: 'Pin to Group' },
pinToGroupSuccess: { id: 'status.pin_to_group.success', defaultMessage: 'Pinned to Group!' },
unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' },
quotePost: { id: 'status.quote', defaultMessage: 'Quote post' },
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' },
@ -93,7 +97,10 @@ const messages = defineMessages({
share: { id: 'status.share', defaultMessage: 'Share' },
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
unmuteGroup: { id: 'group.unmute.long_label', defaultMessage: 'Unmute Group' },
unmuteSuccess: { id: 'group.unmute.success', defaultMessage: 'Unmuted the group' },
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' },
});
interface IStatusActionBar {
@ -115,6 +122,12 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const history = useHistory();
const dispatch = useAppDispatch();
const { group } = useGroup((status.group as Group)?.id as string);
const muteGroup = useMuteGroup(group as Group);
const unmuteGroup = useUnmuteGroup(group as Group);
const isMutingGroup = !!group?.relationship?.muting;
const me = useAppSelector(state => state.me);
const { groupRelationship } = useGroupRelationship(status.group?.id);
const features = useFeatures();
@ -265,6 +278,27 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
dispatch(initMuteModal(status.account as Account));
};
const handleMuteGroupClick: React.EventHandler<React.MouseEvent> = () =>
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 handleUnmuteGroupClick: React.EventHandler<React.MouseEvent> = () => {
unmuteGroup.mutate(undefined, {
onSuccess() {
toast.success(intl.formatMessage(messages.unmuteSuccess));
},
});
};
const handleBlockClick: React.EventHandler<React.MouseEvent> = (e) => {
const account = status.get('account') as Account;
@ -472,6 +506,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
}
menu.push(null);
if (features.groupsMuting && status.group) {
menu.push({
text: isMutingGroup ? intl.formatMessage(messages.unmuteGroup) : intl.formatMessage(messages.muteGroup),
icon: require('@tabler/icons/volume-3.svg'),
action: isMutingGroup ? handleUnmuteGroupClick : handleMuteGroupClick,
});
menu.push(null);
}
menu.push({
text: intl.formatMessage(messages.mute, { name: username }),
action: handleMuteClick,

Wyświetl plik

@ -4,6 +4,7 @@ enum Entities {
ACCOUNTS = 'Accounts',
GROUPS = 'Groups',
GROUP_MEMBERSHIPS = 'GroupMemberships',
GROUP_MUTES = 'GroupMutes',
GROUP_RELATIONSHIPS = 'GroupRelationships',
GROUP_TAGS = 'GroupTags',
PATRON_USERS = 'PatronUsers',

Wyświetl plik

@ -7,7 +7,7 @@ import ScrollableList from 'soapbox/components/scrollable-list';
import { Column, Spinner } from 'soapbox/components/ui';
const messages = defineMessages({
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
heading: { id: 'column.blocks', defaultMessage: 'Blocks' },
});
const Blocks: React.FC = () => {
@ -37,7 +37,8 @@ const Blocks: React.FC = () => {
onLoadMore={fetchNextPage}
hasMore={hasNextPage}
emptyMessage={emptyMessage}
itemClassName='pb-4'
emptyMessageCard={false}
itemClassName='pb-4 last:pb-0'
>
{accounts.map((account) => (
<Account key={account.id} account={account} actionType='blocking' />

Wyświetl plik

@ -59,10 +59,10 @@ describe('<GroupOptionsButton />', () => {
});
});
it('should render null', () => {
it('should render one option for muting the group', () => {
render(<GroupOptionsButton group={group} />);
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(0);
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(1);
});
});

Wyświetl plik

@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { openModal } from 'soapbox/actions/modals';
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
import { useLeaveGroup } from 'soapbox/api/hooks';
import { useLeaveGroup, useMuteGroup, useUnmuteGroup } from 'soapbox/api/hooks';
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
import { IconButton } from 'soapbox/components/ui';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
@ -14,10 +14,17 @@ import type { Account, 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' },
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' },
});
@ -30,11 +37,16 @@ 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;
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
const isInGroup = !!group.relationship?.member;
const isBlocked = group.relationship?.blocked_by;
const isMuting = group.relationship?.muting;
const handleShare = () => {
navigator.share({
@ -45,7 +57,28 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
});
};
const onLeaveGroup = () =>
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),
message: intl.formatMessage(messages.confirmationMessage),
@ -62,14 +95,6 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
const canShare = 'share' in navigator;
const items = [];
if (isMember || isAdmin) {
items.push({
text: intl.formatMessage(messages.report),
icon: require('@tabler/icons/flag.svg'),
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
});
}
if (canShare) {
items.push({
text: intl.formatMessage(messages.share),
@ -78,16 +103,33 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
});
}
if (isInGroup) {
items.push({
text: isMuting ? intl.formatMessage(messages.unmute) : intl.formatMessage(messages.mute),
icon: require('@tabler/icons/volume-3.svg'),
action: isMuting ? handleUnmute : handleMute,
});
}
if (isMember || isAdmin) {
items.push({
text: intl.formatMessage(messages.report),
icon: require('@tabler/icons/flag.svg'),
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
});
}
if (isAdmin) {
items.push(null);
items.push({
text: intl.formatMessage(messages.leave),
icon: require('@tabler/icons/logout.svg'),
action: onLeaveGroup,
action: handleLeave,
});
}
return items;
}, [isMember, isAdmin]);
}, [isMember, isAdmin, isInGroup, isMuting]);
if (isBlocked || menu.length === 0) {
return null;

Wyświetl plik

@ -8,12 +8,12 @@ import GroupActionButton from 'soapbox/features/group/components/group-action-bu
import { Group as GroupEntity } from 'soapbox/types/entities';
import { shortNumberFormat } from 'soapbox/utils/numbers';
interface IGroup {
interface IGroupListItem {
group: GroupEntity
withJoinAction?: boolean
}
const GroupListItem = (props: IGroup) => {
const GroupListItem = (props: IGroupListItem) => {
const { group, withJoinAction = true } = props;
return (

Wyświetl plik

@ -0,0 +1,56 @@
import React from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import { useUnmuteGroup } from 'soapbox/api/hooks';
import GroupAvatar from 'soapbox/components/groups/group-avatar';
import { Button, HStack, Text } from 'soapbox/components/ui';
import { type Group } from 'soapbox/schemas';
import toast from 'soapbox/toast';
interface IGroupListItem {
group: Group
onUnmute(): void
}
const messages = defineMessages({
unmuteSuccess: { id: 'group.unmute.success', defaultMessage: 'Unmuted the group' },
});
const GroupListItem = ({ group, onUnmute }: IGroupListItem) => {
const intl = useIntl();
const unmuteGroup = useUnmuteGroup(group);
const handleUnmute = () => {
unmuteGroup.mutate(undefined, {
onSuccess() {
onUnmute();
toast.success(intl.formatMessage(messages.unmuteSuccess));
},
});
};
return (
<HStack alignItems='center' justifyContent='between'>
<HStack alignItems='center' space={3}>
<GroupAvatar
group={group}
size={42}
/>
<Text
weight='semibold'
size='sm'
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
truncate
/>
</HStack>
<Button theme='primary' type='button' onClick={handleUnmute} size='sm'>
<FormattedMessage id='group.unmute.label' defaultMessage='Unmute' />
</Button>
</HStack>
);
};
export default GroupListItem;

Wyświetl plik

@ -1,48 +1,105 @@
import React from 'react';
import React, { useState } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { useMutes } from 'soapbox/api/hooks';
import Account from 'soapbox/components/account';
import { useMutes, useGroupMutes } from 'soapbox/api/hooks';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Column, Spinner } from 'soapbox/components/ui';
import { Column, Stack, Tabs } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import { useFeatures } from 'soapbox/hooks';
import GroupListItem from './components/group-list-item';
const messages = defineMessages({
heading: { id: 'column.mutes', defaultMessage: 'Muted users' },
heading: { id: 'column.mutes', defaultMessage: 'Mutes' },
});
enum TabItems {
ACCOUNTS = 'ACCOUNTS',
GROUPS = 'GROUPS'
}
const Mutes: React.FC = () => {
const intl = useIntl();
const features = useFeatures();
const {
accounts,
hasNextPage,
fetchNextPage,
isLoading,
hasNextPage: hasNextAccountsPage,
fetchNextPage: fetchNextAccounts,
isLoading: isLoadingAccounts,
} = useMutes();
if (isLoading) {
return (
<Column>
<Spinner />
</Column>
);
}
const {
mutes: groupMutes,
isLoading: isLoadingGroups,
hasNextPage: hasNextGroupsPage,
fetchNextPage: fetchNextGroups,
fetchEntities: fetchMutedGroups,
} = useGroupMutes();
const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
const [activeItem, setActiveItem] = useState<TabItems>(TabItems.ACCOUNTS);
const isAccountsTabSelected = activeItem === TabItems.ACCOUNTS;
const scrollableListProps = {
itemClassName: 'pb-4 last:pb-0',
scrollKey: 'mutes',
emptyMessageCard: false,
};
return (
<Column label={intl.formatMessage(messages.heading)}>
<ScrollableList
scrollKey='mutes'
onLoadMore={fetchNextPage}
hasMore={hasNextPage}
emptyMessage={emptyMessage}
itemClassName='pb-4'
>
{accounts.map((account) => (
<Account key={account.id} account={account} actionType='muting' />
))}
</ScrollableList>
<Stack space={4}>
{features.groupsMuting && (
<Tabs
items={[
{
text: 'Users',
action: () => setActiveItem(TabItems.ACCOUNTS),
name: TabItems.ACCOUNTS,
},
{
text: 'Groups',
action: () => setActiveItem(TabItems.GROUPS),
name: TabItems.GROUPS,
},
]}
activeItem={activeItem}
/>
)}
{isAccountsTabSelected ? (
<ScrollableList
{...scrollableListProps}
isLoading={isLoadingAccounts}
onLoadMore={fetchNextAccounts}
hasMore={hasNextAccountsPage}
emptyMessage={
<FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />
}
>
{accounts.map((accounts) =>
<AccountContainer key={accounts.id} id={accounts.id} actionType='muting' />,
)}
</ScrollableList>
) : (
<ScrollableList
{...scrollableListProps}
isLoading={isLoadingGroups}
onLoadMore={fetchNextGroups}
hasMore={hasNextGroupsPage}
emptyMessage={
<FormattedMessage id='mutes.empty.groups' defaultMessage="You haven't muted any groups yet." />
}
>
{groupMutes.map((group) =>(
<GroupListItem
group={group}
onUnmute={fetchMutedGroups}
/>
))}
</ScrollableList>
)}
</Stack>
</Column>
);
};

Wyświetl plik

@ -12,24 +12,27 @@ import Preferences from '../preferences';
import MessagesSettings from './components/messages-settings';
const messages = defineMessages({
settings: { id: 'settings.settings', defaultMessage: 'Settings' },
profile: { id: 'settings.profile', defaultMessage: 'Profile' },
security: { id: 'settings.security', defaultMessage: 'Security' },
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' },
accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
accountMigration: { id: 'settings.account_migration', defaultMessage: 'Move Account' },
backups: { id: 'column.backups', defaultMessage: 'Backups' },
blocks: { id: 'settings.blocks', defaultMessage: 'Blocks' },
changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' },
changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' },
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' },
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
accountMigration: { id: 'settings.account_migration', defaultMessage: 'Move Account' },
accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
other: { id: 'settings.other', defaultMessage: 'Other options' },
mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' },
mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' },
backups: { id: 'column.backups', defaultMessage: 'Backups' },
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' },
exportData: { id: 'column.export_data', defaultMessage: 'Export data' },
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' },
mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' },
mutes: { id: 'settings.mutes', defaultMessage: 'Mutes' },
other: { id: 'settings.other', defaultMessage: 'Other options' },
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
privacy: { id: 'settings.privacy', defaultMessage: 'Privacy' },
profile: { id: 'settings.profile', defaultMessage: 'Profile' },
security: { id: 'settings.security', defaultMessage: 'Security' },
sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' },
settings: { id: 'settings.settings', defaultMessage: 'Settings' },
});
/** User settings page. */
@ -53,6 +56,8 @@ const Settings = () => {
const navigateToBackups = () => history.push('/settings/backups');
const navigateToImportData = () => history.push('/settings/import');
const navigateToExportData = () => history.push('/settings/export');
const navigateToMutes = () => history.push('/mutes');
const navigateToBlocks = () => history.push('/blocks');
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
@ -79,6 +84,17 @@ const Settings = () => {
</List>
</CardBody>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.privacy)} />
</CardHeader>
<CardBody>
<List>
<ListItem label={intl.formatMessage(messages.mutes)} onClick={navigateToMutes} />
<ListItem label={intl.formatMessage(messages.blocks)} onClick={navigateToBlocks} />
</List>
</CardBody>
{(features.security || features.sessions) && (
<>
<CardHeader>

Wyświetl plik

@ -308,7 +308,7 @@
"column.app_create": "Create app",
"column.backups": "Backups",
"column.birthdays": "Birthdays",
"column.blocks": "Blocked users",
"column.blocks": "Blocks",
"column.bookmarks": "Bookmarks",
"column.chats": "Chats",
"column.community": "Local timeline",
@ -367,7 +367,7 @@
"column.mfa_disable_button": "Disable",
"column.mfa_setup": "Proceed to Setup",
"column.migration": "Account migration",
"column.mutes": "Muted users",
"column.mutes": "Mutes",
"column.notifications": "Notifications",
"column.pins": "Pinned posts",
"column.preferences": "Preferences",
@ -510,6 +510,9 @@
"confirmations.mute.confirm": "Mute",
"confirmations.mute.heading": "Mute @{name}",
"confirmations.mute.message": "Are you sure you want to mute {name}?",
"confirmations.mute_group.confirm": "Mute",
"confirmations.mute_group.heading": "Mute Group",
"confirmations.mute_group.message": "You are about to mute the group. Do you want to continue?",
"confirmations.redraft.confirm": "Delete & redraft",
"confirmations.redraft.heading": "Delete & redraft",
"confirmations.redraft.message": "Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.",
@ -793,6 +796,9 @@
"group.manage": "Manage Group",
"group.member.admin.limit.summary": "You can assign up to {count, plural, one {admin} other {admins}} for the group at this time.",
"group.member.admin.limit.title": "Admin limit reached",
"group.mute.label": "Mute",
"group.mute.long_label": "Mute Group",
"group.mute.success": "Muted the group",
"group.popover.action": "View Group",
"group.popover.summary": "You must be a member of the group in order to reply to this status.",
"group.popover.title": "Membership required",
@ -826,6 +832,9 @@
"group.tags.unpin": "Unpin topic",
"group.tags.unpin.success": "Unpinned!",
"group.tags.visible.success": "Topic marked as visible",
"group.unmute.label": "Unmute",
"group.unmute.long_label": "Unmute Group",
"group.unmute.success": "Unmuted the group",
"group.update.success": "Group successfully saved",
"group.upload_banner": "Upload photo",
"groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.",
@ -1039,6 +1048,7 @@
"mute_modal.auto_expire": "Automatically expire mute?",
"mute_modal.duration": "Duration",
"mute_modal.hide_notifications": "Hide notifications from this user?",
"mutes.empty.groups": "You haven't muted any groups yet.",
"navbar.login.action": "Log in",
"navbar.login.email.placeholder": "E-mail address",
"navbar.login.forgot_password": "Forgot password?",
@ -1351,14 +1361,17 @@
"security.update_password.fail": "Update password failed.",
"security.update_password.success": "Password successfully updated.",
"settings.account_migration": "Move Account",
"settings.blocks": "Blocks",
"settings.change_email": "Change Email",
"settings.change_password": "Change Password",
"settings.configure_mfa": "Configure MFA",
"settings.delete_account": "Delete Account",
"settings.edit_profile": "Edit Profile",
"settings.messages.label": "Allow users to start a new chat with you",
"settings.mutes": "Mutes",
"settings.other": "Other Options",
"settings.preferences": "Preferences",
"settings.privacy": "Privacy",
"settings.profile": "Profile",
"settings.save.success": "Your preferences have been saved!",
"settings.security": "Security",

Wyświetl plik

@ -16,6 +16,7 @@ export const GroupRelationshipRecord = ImmutableRecord({
member: false,
notifying: null,
requested: false,
muting: false,
role: 'user' as GroupRoles,
pending_requests: false,
});

Wyświetl plik

@ -65,10 +65,6 @@ import {
DISLIKES_FETCH_SUCCESS,
REACTIONS_FETCH_SUCCESS,
} from 'soapbox/actions/interactions';
import {
MUTES_FETCH_SUCCESS,
MUTES_EXPAND_SUCCESS,
} from 'soapbox/actions/mutes';
import {
NOTIFICATIONS_UPDATE,
} from 'soapbox/actions/notifications';
@ -203,10 +199,6 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) {
return normalizeList(state, ['blocks'], action.accounts, action.next);
case BLOCKS_EXPAND_SUCCESS:
return appendToList(state, ['blocks'], action.accounts, action.next);
case MUTES_FETCH_SUCCESS:
return normalizeList(state, ['mutes'], action.accounts, action.next);
case MUTES_EXPAND_SUCCESS:
return appendToList(state, ['mutes'], action.accounts, action.next);
case DIRECTORY_FETCH_SUCCESS:
return normalizeList(state, ['directory'], action.accounts, action.next);
case DIRECTORY_EXPAND_SUCCESS:

Wyświetl plik

@ -3,13 +3,14 @@ import z from 'zod';
import { GroupRoles } from './group-member';
const groupRelationshipSchema = z.object({
blocked_by: z.boolean().catch(false),
id: z.string(),
member: z.boolean().catch(false),
requested: z.boolean().catch(false),
role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER),
blocked_by: z.boolean().catch(false),
muting: z.boolean().nullable().catch(false),
notifying: z.boolean().nullable().catch(null),
pending_requests: z.boolean().catch(false),
requested: z.boolean().catch(false),
role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER),
});
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;

Wyświetl plik

@ -567,6 +567,11 @@ const getInstanceFeatures = (instance: Instance) => {
*/
groupsKick: v.software !== TRUTHSOCIAL,
/**
* Can mute a Group.
*/
groupsMuting: v.software === TRUTHSOCIAL,
/**
* Can query pending Group requests.
*/