Merge branch 'group-mutes' into 'develop'
Support Group Muting See merge request soapbox-pub/soapbox!2552
This commit is contained in:
commit
e60a4b5be0
23 changed files with 418 additions and 184 deletions
|
@ -1,96 +1,14 @@
|
||||||
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 { openModal } from './modals';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { Account } from 'soapbox/schemas';
|
||||||
import type { Account as AccountEntity } from 'soapbox/schemas';
|
import type { AppDispatch } from 'soapbox/store';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { Account as AccountEntity } from 'soapbox/types/entities';
|
||||||
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';
|
|
||||||
|
|
||||||
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
||||||
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
||||||
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
|
const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION';
|
||||||
|
|
||||||
const fetchMutes = () =>
|
const initMuteModal = (account: AccountEntity | Account) =>
|
||||||
(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: AppDispatch) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: MUTES_INIT_MODAL,
|
type: MUTES_INIT_MODAL,
|
||||||
|
@ -114,23 +32,9 @@ const changeMuteDuration = (duration: number) =>
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MUTES_FETCH_REQUEST,
|
|
||||||
MUTES_FETCH_SUCCESS,
|
|
||||||
MUTES_FETCH_FAIL,
|
|
||||||
MUTES_EXPAND_REQUEST,
|
|
||||||
MUTES_EXPAND_SUCCESS,
|
|
||||||
MUTES_EXPAND_FAIL,
|
|
||||||
MUTES_INIT_MODAL,
|
MUTES_INIT_MODAL,
|
||||||
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||||
MUTES_CHANGE_DURATION,
|
MUTES_CHANGE_DURATION,
|
||||||
fetchMutes,
|
|
||||||
fetchMutesRequest,
|
|
||||||
fetchMutesSuccess,
|
|
||||||
fetchMutesFail,
|
|
||||||
expandMutes,
|
|
||||||
expandMutesRequest,
|
|
||||||
expandMutesSuccess,
|
|
||||||
expandMutesFail,
|
|
||||||
initMuteModal,
|
initMuteModal,
|
||||||
toggleHideNotifications,
|
toggleHideNotifications,
|
||||||
changeMuteDuration,
|
changeMuteDuration,
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Entities } from 'soapbox/entity-store/entities';
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
import { useEntityActions } from 'soapbox/entity-store/hooks';
|
||||||
|
|
||||||
import type { Group, GroupMember } from 'soapbox/schemas';
|
import type { Account, Group, GroupMember } from 'soapbox/schemas';
|
||||||
|
|
||||||
function useBlockGroupMember(group: Group, groupMember: GroupMember) {
|
function useBlockGroupMember(group: Group, account: Account) {
|
||||||
const { createEntity } = useEntityActions<GroupMember>(
|
const { createEntity } = useEntityActions<GroupMember>(
|
||||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
[Entities.GROUP_MEMBERSHIPS, account.id],
|
||||||
{ post: `/api/v1/groups/${group.id}/blocks` },
|
{ post: `/api/v1/groups/${group?.id}/blocks` },
|
||||||
);
|
);
|
||||||
|
|
||||||
return createEntity;
|
return createEntity;
|
||||||
|
|
|
@ -11,7 +11,11 @@ function useGroup(groupId: string, refetch = true) {
|
||||||
const { entity: group, ...result } = useEntity<Group>(
|
const { entity: group, ...result } = useEntity<Group>(
|
||||||
[Entities.GROUPS, groupId],
|
[Entities.GROUPS, groupId],
|
||||||
() => api.get(`/api/v1/groups/${groupId}`),
|
() => api.get(`/api/v1/groups/${groupId}`),
|
||||||
{ schema: groupSchema, refetch },
|
{
|
||||||
|
schema: groupSchema,
|
||||||
|
refetch,
|
||||||
|
enabled: !!groupId,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const { groupRelationship: relationship } = useGroupRelationship(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 };
|
|
@ -8,7 +8,7 @@ import type { Group, GroupMember } from 'soapbox/schemas';
|
||||||
|
|
||||||
function usePromoteGroupMember(group: Group, groupMember: GroupMember) {
|
function usePromoteGroupMember(group: Group, groupMember: GroupMember) {
|
||||||
const { createEntity } = useEntityActions<GroupMember>(
|
const { createEntity } = useEntityActions<GroupMember>(
|
||||||
[Entities.GROUP_MEMBERSHIPS, groupMember.id],
|
[Entities.GROUP_MEMBERSHIPS, groupMember.account.id],
|
||||||
{ post: `/api/v1/groups/${group.id}/promote` },
|
{ post: `/api/v1/groups/${group.id}/promote` },
|
||||||
{ schema: z.array(groupMemberSchema).transform((arr) => arr[0]) },
|
{ schema: z.array(groupMemberSchema).transform((arr) => arr[0]) },
|
||||||
);
|
);
|
||||||
|
|
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 { useGroupMedia } from './groups/useGroupMedia';
|
||||||
export { useGroupMembers } from './groups/useGroupMembers';
|
export { useGroupMembers } from './groups/useGroupMembers';
|
||||||
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
|
||||||
|
export { useGroupMutes } from './groups/useGroupMutes';
|
||||||
export { useGroupRelationship } from './groups/useGroupRelationship';
|
export { useGroupRelationship } from './groups/useGroupRelationship';
|
||||||
export { useGroupRelationships } from './groups/useGroupRelationships';
|
export { useGroupRelationships } from './groups/useGroupRelationships';
|
||||||
export { useGroupSearch } from './groups/useGroupSearch';
|
export { useGroupSearch } from './groups/useGroupSearch';
|
||||||
|
@ -32,10 +33,12 @@ export { useGroupValidation } from './groups/useGroupValidation';
|
||||||
export { useGroups } from './groups/useGroups';
|
export { useGroups } from './groups/useGroups';
|
||||||
export { useGroupsFromTag } from './groups/useGroupsFromTag';
|
export { useGroupsFromTag } from './groups/useGroupsFromTag';
|
||||||
export { useJoinGroup } from './groups/useJoinGroup';
|
export { useJoinGroup } from './groups/useJoinGroup';
|
||||||
|
export { useMuteGroup } from './groups/useMuteGroup';
|
||||||
export { useLeaveGroup } from './groups/useLeaveGroup';
|
export { useLeaveGroup } from './groups/useLeaveGroup';
|
||||||
export { usePopularGroups } from './groups/usePopularGroups';
|
export { usePopularGroups } from './groups/usePopularGroups';
|
||||||
export { usePopularTags } from './groups/usePopularTags';
|
export { usePopularTags } from './groups/usePopularTags';
|
||||||
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
|
||||||
export { useSuggestedGroups } from './groups/useSuggestedGroups';
|
export { useSuggestedGroups } from './groups/useSuggestedGroups';
|
||||||
|
export { useUnmuteGroup } from './groups/useUnmuteGroup';
|
||||||
export { useUpdateGroup } from './groups/useUpdateGroup';
|
export { useUpdateGroup } from './groups/useUpdateGroup';
|
||||||
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';
|
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
|
|
||||||
import { blockAccount } from 'soapbox/actions/accounts';
|
import { blockAccount } from 'soapbox/actions/accounts';
|
||||||
import { launchChat } from 'soapbox/actions/chats';
|
import { launchChat } from 'soapbox/actions/chats';
|
||||||
|
@ -14,7 +14,7 @@ import { initMuteModal } from 'soapbox/actions/mutes';
|
||||||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||||
import { deleteFromTimelines } from 'soapbox/actions/timelines';
|
import { deleteFromTimelines } from 'soapbox/actions/timelines';
|
||||||
import { useGroupRelationship } from 'soapbox/api/hooks';
|
import { useBlockGroupMember, useGroup, useGroupRelationship, useMuteGroup, useUnmuteGroup } from 'soapbox/api/hooks';
|
||||||
import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus';
|
import { useDeleteGroupStatus } from 'soapbox/api/hooks/groups/useDeleteGroupStatus';
|
||||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||||
|
@ -36,6 +36,7 @@ const messages = defineMessages({
|
||||||
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
|
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
||||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||||
|
blocked: { id: 'group.group_mod_block.success', defaultMessage: '@{name} is banned' },
|
||||||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||||
|
@ -57,6 +58,9 @@ const messages = defineMessages({
|
||||||
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
||||||
external: { id: 'status.external', defaultMessage: 'View post on {domain}' },
|
external: { id: 'status.external', defaultMessage: 'View post on {domain}' },
|
||||||
favourite: { id: 'status.favourite', defaultMessage: 'Like' },
|
favourite: { id: 'status.favourite', defaultMessage: 'Like' },
|
||||||
|
groupBlockConfirm: { id: 'confirmations.block_from_group.confirm', defaultMessage: 'Ban' },
|
||||||
|
groupBlockFromGroupHeading: { id: 'confirmations.block_from_group.heading', defaultMessage: 'Ban From Group' },
|
||||||
|
groupBlockFromGroupMessage: { id: 'confirmations.block_from_group.message', defaultMessage: 'Are you sure you want to ban @{name} from the group?' },
|
||||||
groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' },
|
groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' },
|
||||||
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
|
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
|
||||||
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove post from group' },
|
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove post from group' },
|
||||||
|
@ -65,12 +69,16 @@ const messages = defineMessages({
|
||||||
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
|
||||||
more: { id: 'status.more', defaultMessage: 'More' },
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||||
|
muteConfirm: { id: 'confirmations.mute_group.confirm', defaultMessage: 'Mute' },
|
||||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
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' },
|
open: { id: 'status.open', defaultMessage: 'Expand this post' },
|
||||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||||
pinToGroup: { id: 'status.pin_to_group', defaultMessage: 'Pin to Group' },
|
pinToGroup: { id: 'status.pin_to_group', defaultMessage: 'Pin to Group' },
|
||||||
pinToGroupSuccess: { id: 'status.pin_to_group.success', defaultMessage: 'Pinned 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' },
|
quotePost: { id: 'status.quote', defaultMessage: 'Quote post' },
|
||||||
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
|
reactionCry: { id: 'status.reactions.cry', defaultMessage: 'Sad' },
|
||||||
reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' },
|
reactionHeart: { id: 'status.reactions.heart', defaultMessage: 'Love' },
|
||||||
|
@ -93,7 +101,10 @@ const messages = defineMessages({
|
||||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
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' },
|
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||||
|
unpinFromGroup: { id: 'status.unpin_to_group', defaultMessage: 'Unpin from Group' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IStatusActionBar {
|
interface IStatusActionBar {
|
||||||
|
@ -114,13 +125,20 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const match = useRouteMatch<{ groupSlug: string }>('/group/:groupSlug');
|
||||||
|
|
||||||
|
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 deleteGroupStatus = useDeleteGroupStatus(group as Group, status.id);
|
||||||
|
const blockGroupMember = useBlockGroupMember(group as Group, status?.account as any);
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const { groupRelationship } = useGroupRelationship(status.group?.id);
|
const { groupRelationship } = useGroupRelationship(status.group?.id);
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
const deleteGroupStatus = useDeleteGroupStatus(status?.group as Group, status.id);
|
|
||||||
|
|
||||||
const { allowedEmoji } = soapboxConfig;
|
const { allowedEmoji } = soapboxConfig;
|
||||||
|
|
||||||
|
@ -265,6 +283,27 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
dispatch(initMuteModal(status.account as Account));
|
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 handleBlockClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
const account = status.get('account') as Account;
|
const account = status.get('account') as Account;
|
||||||
|
|
||||||
|
@ -337,6 +376,21 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBlockFromGroup = () => {
|
||||||
|
dispatch(openModal('CONFIRM', {
|
||||||
|
heading: intl.formatMessage(messages.groupBlockFromGroupHeading),
|
||||||
|
message: intl.formatMessage(messages.groupBlockFromGroupMessage, { name: (status.account as any).username }),
|
||||||
|
confirm: intl.formatMessage(messages.groupBlockConfirm),
|
||||||
|
onConfirm: () => {
|
||||||
|
blockGroupMember({ account_ids: [(status.account as any).id] }, {
|
||||||
|
onSuccess() {
|
||||||
|
toast.success(intl.formatMessage(messages.blocked, { name: account?.acct }));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
const _makeMenu = (publicStatus: boolean) => {
|
const _makeMenu = (publicStatus: boolean) => {
|
||||||
const mutingConversation = status.muted;
|
const mutingConversation = status.muted;
|
||||||
const ownAccount = status.account.id === me;
|
const ownAccount = status.account.id === me;
|
||||||
|
@ -472,6 +526,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push(null);
|
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({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.mute, { name: username }),
|
text: intl.formatMessage(messages.mute, { name: username }),
|
||||||
action: handleMuteClick,
|
action: handleMuteClick,
|
||||||
|
@ -495,10 +558,24 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER;
|
const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER;
|
||||||
const isGroupAdmin = groupRelationship?.role === GroupRoles.ADMIN;
|
const isGroupAdmin = groupRelationship?.role === GroupRoles.ADMIN;
|
||||||
const isStatusFromOwner = group.owner.id === account.id;
|
const isStatusFromOwner = group.owner.id === account.id;
|
||||||
|
|
||||||
|
const canBanUser = match?.isExact && (isGroupOwner || isGroupAdmin) && !isStatusFromOwner && !ownAccount;
|
||||||
const canDeleteStatus = !ownAccount && (isGroupOwner || (isGroupAdmin && !isStatusFromOwner));
|
const canDeleteStatus = !ownAccount && (isGroupOwner || (isGroupAdmin && !isStatusFromOwner));
|
||||||
|
|
||||||
if (canDeleteStatus) {
|
if (canBanUser || canDeleteStatus) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canBanUser) {
|
||||||
|
menu.push({
|
||||||
|
text: 'Ban from Group',
|
||||||
|
action: handleBlockFromGroup,
|
||||||
|
icon: require('@tabler/icons/ban.svg'),
|
||||||
|
destructive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canDeleteStatus) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.groupModDelete),
|
text: intl.formatMessage(messages.groupModDelete),
|
||||||
action: handleDeleteFromGroup,
|
action: handleDeleteFromGroup,
|
||||||
|
|
|
@ -4,6 +4,7 @@ enum Entities {
|
||||||
ACCOUNTS = 'Accounts',
|
ACCOUNTS = 'Accounts',
|
||||||
GROUPS = 'Groups',
|
GROUPS = 'Groups',
|
||||||
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
||||||
|
GROUP_MUTES = 'GroupMutes',
|
||||||
GROUP_RELATIONSHIPS = 'GroupRelationships',
|
GROUP_RELATIONSHIPS = 'GroupRelationships',
|
||||||
GROUP_TAGS = 'GroupTags',
|
GROUP_TAGS = 'GroupTags',
|
||||||
PATRON_USERS = 'PatronUsers',
|
PATRON_USERS = 'PatronUsers',
|
||||||
|
|
|
@ -7,7 +7,7 @@ import ScrollableList from 'soapbox/components/scrollable-list';
|
||||||
import { Column, Spinner } from 'soapbox/components/ui';
|
import { Column, Spinner } from 'soapbox/components/ui';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'column.blocks', defaultMessage: 'Blocked users' },
|
heading: { id: 'column.blocks', defaultMessage: 'Blocks' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const Blocks: React.FC = () => {
|
const Blocks: React.FC = () => {
|
||||||
|
@ -37,7 +37,8 @@ const Blocks: React.FC = () => {
|
||||||
onLoadMore={fetchNextPage}
|
onLoadMore={fetchNextPage}
|
||||||
hasMore={hasNextPage}
|
hasMore={hasNextPage}
|
||||||
emptyMessage={emptyMessage}
|
emptyMessage={emptyMessage}
|
||||||
itemClassName='pb-4'
|
emptyMessageCard={false}
|
||||||
|
itemClassName='pb-4 last:pb-0'
|
||||||
>
|
>
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
<Account key={account.id} account={account} actionType='blocking' />
|
<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} />);
|
render(<GroupOptionsButton group={group} />);
|
||||||
|
|
||||||
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(0);
|
expect(screen.queryAllByTestId('dropdown-menu-button')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ const GroupMemberListItem = (props: IGroupMemberListItem) => {
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const blockGroupMember = useBlockGroupMember(group, member);
|
const blockGroupMember = useBlockGroupMember(group, member.account);
|
||||||
const promoteGroupMember = usePromoteGroupMember(group, member);
|
const promoteGroupMember = usePromoteGroupMember(group, member);
|
||||||
const demoteGroupMember = useDemoteGroupMember(group, member);
|
const demoteGroupMember = useDemoteGroupMember(group, member);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
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 DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
|
||||||
import { IconButton } from 'soapbox/components/ui';
|
import { IconButton } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||||
|
@ -14,10 +14,17 @@ import type { Account, Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' },
|
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?' },
|
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' },
|
leave: { id: 'group.leave.label', defaultMessage: 'Leave' },
|
||||||
leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' },
|
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' },
|
report: { id: 'group.report.label', defaultMessage: 'Report' },
|
||||||
share: { id: 'group.share.label', defaultMessage: 'Share' },
|
share: { id: 'group.share.label', defaultMessage: 'Share' },
|
||||||
});
|
});
|
||||||
|
@ -30,11 +37,16 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
|
||||||
const { account } = useOwnAccount();
|
const { account } = useOwnAccount();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const muteGroup = useMuteGroup(group);
|
||||||
|
const unmuteGroup = useUnmuteGroup(group);
|
||||||
const leaveGroup = useLeaveGroup(group);
|
const leaveGroup = useLeaveGroup(group);
|
||||||
|
|
||||||
const isMember = group.relationship?.role === GroupRoles.USER;
|
const isMember = group.relationship?.role === GroupRoles.USER;
|
||||||
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
|
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
|
||||||
|
const isInGroup = !!group.relationship?.member;
|
||||||
const isBlocked = group.relationship?.blocked_by;
|
const isBlocked = group.relationship?.blocked_by;
|
||||||
|
const isMuting = group.relationship?.muting;
|
||||||
|
|
||||||
const handleShare = () => {
|
const handleShare = () => {
|
||||||
navigator.share({
|
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', {
|
dispatch(openModal('CONFIRM', {
|
||||||
heading: intl.formatMessage(messages.confirmationHeading),
|
heading: intl.formatMessage(messages.confirmationHeading),
|
||||||
message: intl.formatMessage(messages.confirmationMessage),
|
message: intl.formatMessage(messages.confirmationMessage),
|
||||||
|
@ -62,14 +95,6 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => {
|
||||||
const canShare = 'share' in navigator;
|
const canShare = 'share' in navigator;
|
||||||
const items = [];
|
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) {
|
if (canShare) {
|
||||||
items.push({
|
items.push({
|
||||||
text: intl.formatMessage(messages.share),
|
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) {
|
if (isAdmin) {
|
||||||
|
items.push(null);
|
||||||
items.push({
|
items.push({
|
||||||
text: intl.formatMessage(messages.leave),
|
text: intl.formatMessage(messages.leave),
|
||||||
icon: require('@tabler/icons/logout.svg'),
|
icon: require('@tabler/icons/logout.svg'),
|
||||||
action: onLeaveGroup,
|
action: handleLeave,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}, [isMember, isAdmin]);
|
}, [isMember, isAdmin, isInGroup, isMuting]);
|
||||||
|
|
||||||
if (isBlocked || menu.length === 0) {
|
if (isBlocked || menu.length === 0) {
|
||||||
return null;
|
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 { Group as GroupEntity } from 'soapbox/types/entities';
|
||||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||||
|
|
||||||
interface IGroup {
|
interface IGroupListItem {
|
||||||
group: GroupEntity
|
group: GroupEntity
|
||||||
withJoinAction?: boolean
|
withJoinAction?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupListItem = (props: IGroup) => {
|
const GroupListItem = (props: IGroupListItem) => {
|
||||||
const { group, withJoinAction = true } = props;
|
const { group, withJoinAction = true } = props;
|
||||||
|
|
||||||
return (
|
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 { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useMutes } from 'soapbox/api/hooks';
|
import { useMutes, useGroupMutes } from 'soapbox/api/hooks';
|
||||||
import Account from 'soapbox/components/account';
|
|
||||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
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({
|
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 Mutes: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
accounts,
|
accounts,
|
||||||
hasNextPage,
|
hasNextPage: hasNextAccountsPage,
|
||||||
fetchNextPage,
|
fetchNextPage: fetchNextAccounts,
|
||||||
isLoading,
|
isLoading: isLoadingAccounts,
|
||||||
} = useMutes();
|
} = useMutes();
|
||||||
|
|
||||||
if (isLoading) {
|
const {
|
||||||
return (
|
mutes: groupMutes,
|
||||||
<Column>
|
isLoading: isLoadingGroups,
|
||||||
<Spinner />
|
hasNextPage: hasNextGroupsPage,
|
||||||
</Column>
|
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 (
|
return (
|
||||||
<Column label={intl.formatMessage(messages.heading)}>
|
<Column label={intl.formatMessage(messages.heading)}>
|
||||||
<ScrollableList
|
<Stack space={4}>
|
||||||
scrollKey='mutes'
|
{features.groupsMuting && (
|
||||||
onLoadMore={fetchNextPage}
|
<Tabs
|
||||||
hasMore={hasNextPage}
|
items={[
|
||||||
emptyMessage={emptyMessage}
|
{
|
||||||
itemClassName='pb-4'
|
text: 'Users',
|
||||||
>
|
action: () => setActiveItem(TabItems.ACCOUNTS),
|
||||||
{accounts.map((account) => (
|
name: TabItems.ACCOUNTS,
|
||||||
<Account key={account.id} account={account} actionType='muting' />
|
},
|
||||||
))}
|
{
|
||||||
</ScrollableList>
|
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>
|
</Column>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,24 +12,27 @@ import Preferences from '../preferences';
|
||||||
import MessagesSettings from './components/messages-settings';
|
import MessagesSettings from './components/messages-settings';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
settings: { id: 'settings.settings', defaultMessage: 'Settings' },
|
accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
|
||||||
profile: { id: 'settings.profile', defaultMessage: 'Profile' },
|
accountMigration: { id: 'settings.account_migration', defaultMessage: 'Move Account' },
|
||||||
security: { id: 'settings.security', defaultMessage: 'Security' },
|
backups: { id: 'column.backups', defaultMessage: 'Backups' },
|
||||||
preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' },
|
blocks: { id: 'settings.blocks', defaultMessage: 'Blocks' },
|
||||||
editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' },
|
|
||||||
changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' },
|
changeEmail: { id: 'settings.change_email', defaultMessage: 'Change Email' },
|
||||||
changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' },
|
changePassword: { id: 'settings.change_password', defaultMessage: 'Change Password' },
|
||||||
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
|
configureMfa: { id: 'settings.configure_mfa', defaultMessage: 'Configure MFA' },
|
||||||
sessions: { id: 'settings.sessions', defaultMessage: 'Active sessions' },
|
|
||||||
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
|
deleteAccount: { id: 'settings.delete_account', defaultMessage: 'Delete Account' },
|
||||||
accountMigration: { id: 'settings.account_migration', defaultMessage: 'Move Account' },
|
editProfile: { id: 'settings.edit_profile', defaultMessage: 'Edit Profile' },
|
||||||
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' },
|
|
||||||
exportData: { id: 'column.export_data', defaultMessage: 'Export data' },
|
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. */
|
/** User settings page. */
|
||||||
|
@ -53,6 +56,8 @@ const Settings = () => {
|
||||||
const navigateToBackups = () => history.push('/settings/backups');
|
const navigateToBackups = () => history.push('/settings/backups');
|
||||||
const navigateToImportData = () => history.push('/settings/import');
|
const navigateToImportData = () => history.push('/settings/import');
|
||||||
const navigateToExportData = () => history.push('/settings/export');
|
const navigateToExportData = () => history.push('/settings/export');
|
||||||
|
const navigateToMutes = () => history.push('/mutes');
|
||||||
|
const navigateToBlocks = () => history.push('/blocks');
|
||||||
|
|
||||||
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
|
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
|
||||||
|
|
||||||
|
@ -79,6 +84,17 @@ const Settings = () => {
|
||||||
</List>
|
</List>
|
||||||
</CardBody>
|
</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) && (
|
{(features.security || features.sessions) && (
|
||||||
<>
|
<>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|
|
@ -308,7 +308,7 @@
|
||||||
"column.app_create": "Create app",
|
"column.app_create": "Create app",
|
||||||
"column.backups": "Backups",
|
"column.backups": "Backups",
|
||||||
"column.birthdays": "Birthdays",
|
"column.birthdays": "Birthdays",
|
||||||
"column.blocks": "Blocked users",
|
"column.blocks": "Blocks",
|
||||||
"column.bookmarks": "Bookmarks",
|
"column.bookmarks": "Bookmarks",
|
||||||
"column.chats": "Chats",
|
"column.chats": "Chats",
|
||||||
"column.community": "Local timeline",
|
"column.community": "Local timeline",
|
||||||
|
@ -367,7 +367,7 @@
|
||||||
"column.mfa_disable_button": "Disable",
|
"column.mfa_disable_button": "Disable",
|
||||||
"column.mfa_setup": "Proceed to Setup",
|
"column.mfa_setup": "Proceed to Setup",
|
||||||
"column.migration": "Account migration",
|
"column.migration": "Account migration",
|
||||||
"column.mutes": "Muted users",
|
"column.mutes": "Mutes",
|
||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
"column.pins": "Pinned posts",
|
"column.pins": "Pinned posts",
|
||||||
"column.preferences": "Preferences",
|
"column.preferences": "Preferences",
|
||||||
|
@ -510,6 +510,9 @@
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
"confirmations.mute.heading": "Mute @{name}",
|
"confirmations.mute.heading": "Mute @{name}",
|
||||||
"confirmations.mute.message": "Are you sure you want to 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.confirm": "Delete & redraft",
|
||||||
"confirmations.redraft.heading": "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.",
|
"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.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.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.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.action": "View Group",
|
||||||
"group.popover.summary": "You must be a member of the group in order to reply to this status.",
|
"group.popover.summary": "You must be a member of the group in order to reply to this status.",
|
||||||
"group.popover.title": "Membership required",
|
"group.popover.title": "Membership required",
|
||||||
|
@ -826,6 +832,9 @@
|
||||||
"group.tags.unpin": "Unpin topic",
|
"group.tags.unpin": "Unpin topic",
|
||||||
"group.tags.unpin.success": "Unpinned!",
|
"group.tags.unpin.success": "Unpinned!",
|
||||||
"group.tags.visible.success": "Topic marked as visible",
|
"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.update.success": "Group successfully saved",
|
||||||
"group.upload_banner": "Upload photo",
|
"group.upload_banner": "Upload photo",
|
||||||
"groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.",
|
"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.auto_expire": "Automatically expire mute?",
|
||||||
"mute_modal.duration": "Duration",
|
"mute_modal.duration": "Duration",
|
||||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
"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.action": "Log in",
|
||||||
"navbar.login.email.placeholder": "E-mail address",
|
"navbar.login.email.placeholder": "E-mail address",
|
||||||
"navbar.login.forgot_password": "Forgot password?",
|
"navbar.login.forgot_password": "Forgot password?",
|
||||||
|
@ -1351,14 +1361,17 @@
|
||||||
"security.update_password.fail": "Update password failed.",
|
"security.update_password.fail": "Update password failed.",
|
||||||
"security.update_password.success": "Password successfully updated.",
|
"security.update_password.success": "Password successfully updated.",
|
||||||
"settings.account_migration": "Move Account",
|
"settings.account_migration": "Move Account",
|
||||||
|
"settings.blocks": "Blocks",
|
||||||
"settings.change_email": "Change Email",
|
"settings.change_email": "Change Email",
|
||||||
"settings.change_password": "Change Password",
|
"settings.change_password": "Change Password",
|
||||||
"settings.configure_mfa": "Configure MFA",
|
"settings.configure_mfa": "Configure MFA",
|
||||||
"settings.delete_account": "Delete Account",
|
"settings.delete_account": "Delete Account",
|
||||||
"settings.edit_profile": "Edit Profile",
|
"settings.edit_profile": "Edit Profile",
|
||||||
"settings.messages.label": "Allow users to start a new chat with you",
|
"settings.messages.label": "Allow users to start a new chat with you",
|
||||||
|
"settings.mutes": "Mutes",
|
||||||
"settings.other": "Other Options",
|
"settings.other": "Other Options",
|
||||||
"settings.preferences": "Preferences",
|
"settings.preferences": "Preferences",
|
||||||
|
"settings.privacy": "Privacy",
|
||||||
"settings.profile": "Profile",
|
"settings.profile": "Profile",
|
||||||
"settings.save.success": "Your preferences have been saved!",
|
"settings.save.success": "Your preferences have been saved!",
|
||||||
"settings.security": "Security",
|
"settings.security": "Security",
|
||||||
|
|
|
@ -16,6 +16,7 @@ export const GroupRelationshipRecord = ImmutableRecord({
|
||||||
member: false,
|
member: false,
|
||||||
notifying: null,
|
notifying: null,
|
||||||
requested: false,
|
requested: false,
|
||||||
|
muting: false,
|
||||||
role: 'user' as GroupRoles,
|
role: 'user' as GroupRoles,
|
||||||
pending_requests: false,
|
pending_requests: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -65,10 +65,6 @@ import {
|
||||||
DISLIKES_FETCH_SUCCESS,
|
DISLIKES_FETCH_SUCCESS,
|
||||||
REACTIONS_FETCH_SUCCESS,
|
REACTIONS_FETCH_SUCCESS,
|
||||||
} from 'soapbox/actions/interactions';
|
} from 'soapbox/actions/interactions';
|
||||||
import {
|
|
||||||
MUTES_FETCH_SUCCESS,
|
|
||||||
MUTES_EXPAND_SUCCESS,
|
|
||||||
} from 'soapbox/actions/mutes';
|
|
||||||
import {
|
import {
|
||||||
NOTIFICATIONS_UPDATE,
|
NOTIFICATIONS_UPDATE,
|
||||||
} from 'soapbox/actions/notifications';
|
} from 'soapbox/actions/notifications';
|
||||||
|
@ -203,10 +199,6 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) {
|
||||||
return normalizeList(state, ['blocks'], action.accounts, action.next);
|
return normalizeList(state, ['blocks'], action.accounts, action.next);
|
||||||
case BLOCKS_EXPAND_SUCCESS:
|
case BLOCKS_EXPAND_SUCCESS:
|
||||||
return appendToList(state, ['blocks'], action.accounts, action.next);
|
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:
|
case DIRECTORY_FETCH_SUCCESS:
|
||||||
return normalizeList(state, ['directory'], action.accounts, action.next);
|
return normalizeList(state, ['directory'], action.accounts, action.next);
|
||||||
case DIRECTORY_EXPAND_SUCCESS:
|
case DIRECTORY_EXPAND_SUCCESS:
|
||||||
|
|
|
@ -3,13 +3,14 @@ import z from 'zod';
|
||||||
import { GroupRoles } from './group-member';
|
import { GroupRoles } from './group-member';
|
||||||
|
|
||||||
const groupRelationshipSchema = z.object({
|
const groupRelationshipSchema = z.object({
|
||||||
|
blocked_by: z.boolean().catch(false),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
member: z.boolean().catch(false),
|
member: z.boolean().catch(false),
|
||||||
requested: z.boolean().catch(false),
|
muting: z.boolean().nullable().catch(false),
|
||||||
role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER),
|
|
||||||
blocked_by: z.boolean().catch(false),
|
|
||||||
notifying: z.boolean().nullable().catch(null),
|
notifying: z.boolean().nullable().catch(null),
|
||||||
pending_requests: z.boolean().catch(false),
|
pending_requests: z.boolean().catch(false),
|
||||||
|
requested: z.boolean().catch(false),
|
||||||
|
role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER),
|
||||||
});
|
});
|
||||||
|
|
||||||
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;
|
type GroupRelationship = z.infer<typeof groupRelationshipSchema>;
|
||||||
|
|
|
@ -567,6 +567,11 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
groupsKick: v.software !== TRUTHSOCIAL,
|
groupsKick: v.software !== TRUTHSOCIAL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can mute a Group.
|
||||||
|
*/
|
||||||
|
groupsMuting: v.software === TRUTHSOCIAL,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can query pending Group requests.
|
* Can query pending Group requests.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in a new issue