Support Group mutes
This commit is contained in:
parent
e58dada03a
commit
e3fa58c0da
20 changed files with 373 additions and 174 deletions
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
25
app/soapbox/api/hooks/groups/useGroupMutes.ts
Normal file
25
app/soapbox/api/hooks/groups/useGroupMutes.ts
Normal file
|
@ -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 };
|
18
app/soapbox/api/hooks/groups/useMuteGroup.ts
Normal file
18
app/soapbox/api/hooks/groups/useMuteGroup.ts
Normal file
|
@ -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 };
|
18
app/soapbox/api/hooks/groups/useUnmuteGroup.ts
Normal file
18
app/soapbox/api/hooks/groups/useUnmuteGroup.ts
Normal file
|
@ -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 };
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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' />
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 (
|
||||
|
|
56
app/soapbox/features/mutes/components/group-list-item.tsx
Normal file
56
app/soapbox/features/mutes/components/group-list-item.tsx
Normal file
|
@ -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;
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -16,6 +16,7 @@ export const GroupRelationshipRecord = ImmutableRecord({
|
|||
member: false,
|
||||
notifying: null,
|
||||
requested: false,
|
||||
muting: false,
|
||||
role: 'user' as GroupRoles,
|
||||
pending_requests: false,
|
||||
});
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
Loading…
Reference in a new issue