Support mutes duration

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-07-31 11:57:31 +02:00
parent 30ec3dbd9f
commit ac83c9c18c
8 changed files with 95 additions and 6 deletions

View file

@ -1,5 +1,5 @@
import { isLoggedIn } from 'soapbox/utils/auth'; import { isLoggedIn } from 'soapbox/utils/auth';
import { getFeatures } from 'soapbox/utils/features'; import { getFeatures, parseVersion, PLEROMA } from 'soapbox/utils/features';
import api, { getLinks } from '../api'; import api, { getLinks } from '../api';
@ -354,14 +354,30 @@ const unblockAccountFail = (error: AxiosError) => ({
error, error,
}); });
const muteAccount = (id: string, notifications?: boolean) => const muteAccount = (id: string, notifications?: boolean, duration = 0) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
if (!isLoggedIn(getState)) return null; if (!isLoggedIn(getState)) return null;
dispatch(muteAccountRequest(id)); dispatch(muteAccountRequest(id));
const params: Record<string, any> = {
notifications,
};
if (duration) {
const state = getState();
const instance = state.instance;
const v = parseVersion(instance.version);
if (v.software === PLEROMA) {
params.expires_in = duration;
} else {
params.duration = duration;
}
}
return api(getState) return api(getState)
.post(`/api/v1/accounts/${id}/mute`, { notifications }) .post(`/api/v1/accounts/${id}/mute`, params)
.then(response => { .then(response => {
// Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers
return dispatch(muteAccountSuccess(response.data, getState().statuses)); return dispatch(muteAccountSuccess(response.data, getState().statuses));

View file

@ -21,6 +21,7 @@ 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 fetchMutes = () => const fetchMutes = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
@ -103,6 +104,14 @@ const toggleHideNotifications = () =>
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
}; };
const changeMuteDuration = (duration: number) =>
(dispatch: AppDispatch) => {
dispatch({
type: MUTES_CHANGE_DURATION,
duration,
});
};
export { export {
MUTES_FETCH_REQUEST, MUTES_FETCH_REQUEST,
MUTES_FETCH_SUCCESS, MUTES_FETCH_SUCCESS,
@ -112,6 +121,7 @@ export {
MUTES_EXPAND_FAIL, MUTES_EXPAND_FAIL,
MUTES_INIT_MODAL, MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS, MUTES_TOGGLE_HIDE_NOTIFICATIONS,
MUTES_CHANGE_DURATION,
fetchMutes, fetchMutes,
fetchMutesRequest, fetchMutesRequest,
fetchMutesSuccess, fetchMutesSuccess,
@ -122,4 +132,5 @@ export {
expandMutesFail, expandMutesFail,
initMuteModal, initMuteModal,
toggleHideNotifications, toggleHideNotifications,
changeMuteDuration,
}; };

View file

@ -235,6 +235,14 @@ const Account = ({
<Icon className='h-5 w-5 stroke-[1.35]' src={require('@tabler/icons/pencil.svg')} /> <Icon className='h-5 w-5 stroke-[1.35]' src={require('@tabler/icons/pencil.svg')} />
</> </>
) : null} ) : null}
{actionType === 'muting' && account.mute_expires_at ? (
<>
<Text tag='span' theme='muted' size='sm'>&middot;</Text>
<Text theme='muted' size='sm'><RelativeTimestamp timestamp={account.mute_expires_at} futureDate /></Text>
</>
) : null}
</HStack> </HStack>
{withAccountNote && ( {withAccountNote && (

View file

@ -4,9 +4,10 @@ import Toggle from 'react-toggle';
import { muteAccount } from 'soapbox/actions/accounts'; import { muteAccount } from 'soapbox/actions/accounts';
import { closeModal } from 'soapbox/actions/modals'; import { closeModal } from 'soapbox/actions/modals';
import { toggleHideNotifications } from 'soapbox/actions/mutes'; import { toggleHideNotifications, changeMuteDuration } from 'soapbox/actions/mutes';
import { Modal, HStack, Stack, Text } from 'soapbox/components/ui'; import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import DurationSelector from 'soapbox/features/compose/components/polls/duration-selector';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors'; import { makeGetAccount } from 'soapbox/selectors';
const getAccount = makeGetAccount(); const getAccount = makeGetAccount();
@ -16,12 +17,14 @@ const MuteModal = () => {
const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!)); const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!));
const notifications = useAppSelector((state) => state.mutes.new.notifications); const notifications = useAppSelector((state) => state.mutes.new.notifications);
const duration = useAppSelector((state) => state.mutes.new.duration);
const mutesDuration = useFeatures().mutesDuration;
if (!account) return null; if (!account) return null;
const handleClick = () => { const handleClick = () => {
dispatch(closeModal()); dispatch(closeModal());
dispatch(muteAccount(account.id, notifications)); dispatch(muteAccount(account.id, notifications, duration));
}; };
const handleCancel = () => { const handleCancel = () => {
@ -32,6 +35,12 @@ const MuteModal = () => {
dispatch(toggleHideNotifications()); dispatch(toggleHideNotifications());
}; };
const handleChangeMuteDuration = (expiresIn: number): void => {
dispatch(changeMuteDuration(expiresIn));
};
const toggleAutoExpire = () => handleChangeMuteDuration(duration ? 0 : 2 * 60 * 60 * 24);
return ( return (
<Modal <Modal
title={ title={
@ -69,6 +78,32 @@ const MuteModal = () => {
/> />
</HStack> </HStack>
</label> </label>
{mutesDuration && (
<>
<label>
<HStack alignItems='center' space={2}>
<Text tag='span'>
<FormattedMessage id='mute_modal.auto_expire' defaultMessage='Automatically expire mute?' />
</Text>
<Toggle
checked={duration !== 0}
onChange={toggleAutoExpire}
icons={false}
/>
</HStack>
</label>
{duration !== 0 && (
<Stack space={2}>
<Text weight='medium'><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </Text>
<DurationSelector onDurationChange={handleChangeMuteDuration} />
</Stack>
)}
</>
)}
</Stack> </Stack>
</Modal> </Modal>
); );

View file

@ -42,6 +42,7 @@ export const AccountRecord = ImmutableRecord({
location: '', location: '',
locked: false, locked: false,
moved: null as EmbeddedEntity<any>, moved: null as EmbeddedEntity<any>,
mute_expires_at: null as Date | null,
note: '', note: '',
pleroma: ImmutableMap<string, any>(), pleroma: ImmutableMap<string, any>(),
source: ImmutableMap<string, any>(), source: ImmutableMap<string, any>(),

View file

@ -14,6 +14,7 @@ describe('mutes reducer', () => {
isSubmitting: false, isSubmitting: false,
accountId: null, accountId: null,
notifications: true, notifications: true,
duration: 0,
}, },
}); });
}); });
@ -24,6 +25,7 @@ describe('mutes reducer', () => {
isSubmitting: false, isSubmitting: false,
accountId: null, accountId: null,
notifications: true, notifications: true,
duration: 0,
})(), })(),
})(); })();
const action = { const action = {
@ -35,6 +37,7 @@ describe('mutes reducer', () => {
isSubmitting: false, isSubmitting: false,
accountId: 'account1', accountId: 'account1',
notifications: true, notifications: true,
duration: 0,
}, },
}); });
}); });
@ -45,6 +48,7 @@ describe('mutes reducer', () => {
isSubmitting: false, isSubmitting: false,
accountId: null, accountId: null,
notifications: true, notifications: true,
duration: 0,
})(), })(),
})(); })();
const action = { const action = {
@ -55,6 +59,7 @@ describe('mutes reducer', () => {
isSubmitting: false, isSubmitting: false,
accountId: null, accountId: null,
notifications: false, notifications: false,
duration: 0,
}, },
}); });
}); });

View file

@ -3,6 +3,7 @@ import { Record as ImmutableRecord } from 'immutable';
import { import {
MUTES_INIT_MODAL, MUTES_INIT_MODAL,
MUTES_TOGGLE_HIDE_NOTIFICATIONS, MUTES_TOGGLE_HIDE_NOTIFICATIONS,
MUTES_CHANGE_DURATION,
} from '../actions/mutes'; } from '../actions/mutes';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
@ -11,6 +12,7 @@ const NewMuteRecord = ImmutableRecord({
isSubmitting: false, isSubmitting: false,
accountId: null, accountId: null,
notifications: true, notifications: true,
duration: 0,
}); });
const ReducerRecord = ImmutableRecord({ const ReducerRecord = ImmutableRecord({
@ -29,6 +31,8 @@ export default function mutes(state: State = ReducerRecord(), action: AnyAction)
}); });
case MUTES_TOGGLE_HIDE_NOTIFICATIONS: case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
return state.updateIn(['new', 'notifications'], (old) => !old); return state.updateIn(['new', 'notifications'], (old) => !old);
case MUTES_CHANGE_DURATION:
return state.setIn(['new', 'duration'], action.duration);
default: default:
return state; return state;
} }

View file

@ -391,6 +391,15 @@ const getInstanceFeatures = (instance: Instance) => {
*/ */
muteStrangers: v.software === PLEROMA, muteStrangers: v.software === PLEROMA,
/**
* Ability to specify how long the account mute should last.
* @see PUT /api/v1/accounts/:id/mute
*/
mutesDuration: any([
v.software === PLEROMA && gte(v.version, '2.3.0'),
v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
]),
/** /**
* Add private notes to accounts. * Add private notes to accounts.
* @see POST /api/v1/accounts/:id/note * @see POST /api/v1/accounts/:id/note