Compare commits
42 Commits
main
...
for-upstre
Author | SHA1 | Date |
---|---|---|
Eugen Rochko | e168e8afe5 | |
aschmitz | 75c21adfbf | |
aschmitz | 685fafcc27 | |
aschmitz | 0e210b5c63 | |
aschmitz | 6b70c2ca12 | |
Surinna Curtis | 8c6ecd5616 | |
Surinna Curtis | 22cf9bcff6 | |
Surinna Curtis | 21de45c7d2 | |
Surinna Curtis | 88e52f3a2a | |
Surinna Curtis | d4bb04c45c | |
Surinna Curtis | 33c56212b9 | |
Surinna Curtis | bcd4b72223 | |
Surinna Curtis | f39bba9a90 | |
Surinna Curtis | 290c6b0f2e | |
Surinna Curtis | d5d1dcab77 | |
Surinna Curtis | ef5ebdd544 | |
Surinna Curtis | da85bfc252 | |
Surinna Curtis | d17255c0e0 | |
Surinna Curtis | 74ce229101 | |
Surinna Curtis | fb8613d09b | |
Surinna Curtis | d73f437419 | |
Surinna Curtis | 343c358bb2 | |
Surinna Curtis | 8a6ad735f1 | |
Surinna Curtis | 9fb8a6f231 | |
Surinna Curtis | f90ac53dc5 | |
Surinna Curtis | c814d19672 | |
Surinna Curtis | 27eb75878e | |
Surinna Curtis | a81d3a2842 | |
Surinna Curtis | 1fc0382d29 | |
Surinna Curtis | 90db8b63b0 | |
Surinna Curtis | 15a6cb5ca9 | |
Surinna Curtis | d5dc4696a7 | |
Surinna Curtis | eb6543b36b | |
Surinna Curtis | 920c7cdaf3 | |
Surinna Curtis | f3aac23099 | |
Surinna Curtis | aded1acfda | |
Surinna Curtis | 9855529a10 | |
Surinna Curtis | 0ccfbe5747 | |
Surinna Curtis | 921a1b7b2a | |
Surinna Curtis | aa7e541780 | |
Surinna Curtis | dac67960e0 | |
Surinna Curtis | 90a0176b0b |
|
@ -26,7 +26,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def mute
|
def mute
|
||||||
MuteService.new.call(current_user.account, @account)
|
MuteService.new.call(current_user.account, @account, notifications: params[:notifications])
|
||||||
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -241,11 +241,11 @@ export function unblockAccountFail(error) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export function muteAccount(id) {
|
export function muteAccount(id, notifications) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(muteAccountRequest(id));
|
dispatch(muteAccountRequest(id));
|
||||||
|
|
||||||
api(getState).post(`/api/v1/accounts/${id}/mute`).then(response => {
|
api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).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
|
||||||
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
|
dispatch(muteAccountSuccess(response.data, getState().get('statuses')));
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import { fetchRelationships } from './accounts';
|
import { fetchRelationships } from './accounts';
|
||||||
|
import { openModal } from '../../mastodon/actions/modal';
|
||||||
|
|
||||||
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
|
||||||
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS';
|
||||||
|
@ -9,6 +10,9 @@ export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST';
|
||||||
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS';
|
||||||
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL';
|
||||||
|
|
||||||
|
export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL';
|
||||||
|
export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS';
|
||||||
|
|
||||||
export function fetchMutes() {
|
export function fetchMutes() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchMutesRequest());
|
dispatch(fetchMutesRequest());
|
||||||
|
@ -80,3 +84,20 @@ export function expandMutesFail(error) {
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function initMuteModal(account) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({
|
||||||
|
type: MUTES_INIT_MODAL,
|
||||||
|
account,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(openModal('MUTE'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleHideNotifications() {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS });
|
||||||
|
};
|
||||||
|
}
|
|
@ -15,6 +15,8 @@ const messages = defineMessages({
|
||||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||||
|
mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'Mute notifications from @{name}' },
|
||||||
|
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
@injectIntl
|
@injectIntl
|
||||||
|
@ -41,6 +43,14 @@ export default class Account extends ImmutablePureComponent {
|
||||||
this.props.onMute(this.props.account);
|
this.props.onMute(this.props.account);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMuteNotifications = () => {
|
||||||
|
this.props.onMuteNotifications(this.props.account, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUnmuteNotifications = () => {
|
||||||
|
this.props.onMuteNotifications(this.props.account, false);
|
||||||
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, intl, hidden } = this.props;
|
const { account, intl, hidden } = this.props;
|
||||||
|
|
||||||
|
@ -70,7 +80,18 @@ export default class Account extends ImmutablePureComponent {
|
||||||
} else if (blocking) {
|
} else if (blocking) {
|
||||||
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
buttons = <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||||
} else if (muting) {
|
} else if (muting) {
|
||||||
buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
|
let hidingNotificationsButton;
|
||||||
|
if (muting.get('notifications')) {
|
||||||
|
hidingNotificationsButton = <IconButton active icon='bell' title={intl.formatMessage(messages.unmute_notifications, { name: account.get('username') })} onClick={this.handleUnmuteNotifications} />;
|
||||||
|
} else {
|
||||||
|
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
|
||||||
|
}
|
||||||
|
buttons = (
|
||||||
|
<div>
|
||||||
|
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
|
||||||
|
{hidingNotificationsButton}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
|
import { initMuteModal } from '../actions/mutes';
|
||||||
import { unfollowModal } from '../initial_state';
|
import { unfollowModal } from '../initial_state';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -58,10 +59,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (account.getIn(['relationship', 'muting'])) {
|
if (account.getIn(['relationship', 'muting'])) {
|
||||||
dispatch(unmuteAccount(account.get('id')));
|
dispatch(unmuteAccount(account.get('id')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(muteAccount(account.get('id')));
|
dispatch(initMuteModal(account));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
onMuteNotifications (account, notifications) {
|
||||||
|
dispatch(muteAccount(account.get('id'), notifications));
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
|
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account));
|
||||||
|
|
|
@ -14,11 +14,9 @@ import {
|
||||||
pin,
|
pin,
|
||||||
unpin,
|
unpin,
|
||||||
} from '../actions/interactions';
|
} from '../actions/interactions';
|
||||||
import {
|
import { blockAccount } from '../actions/accounts';
|
||||||
blockAccount,
|
|
||||||
muteAccount,
|
|
||||||
} from '../actions/accounts';
|
|
||||||
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
|
import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
|
||||||
|
import { initMuteModal } from '../actions/mutes';
|
||||||
import { initReport } from '../actions/reports';
|
import { initReport } from '../actions/reports';
|
||||||
import { openModal } from '../actions/modal';
|
import { openModal } from '../actions/modal';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
@ -28,7 +26,6 @@ const messages = defineMessages({
|
||||||
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
||||||
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
|
@ -120,11 +117,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
},
|
},
|
||||||
|
|
||||||
onMute (account) {
|
onMute (account) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initMuteModal(account));
|
||||||
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.muteConfirm),
|
|
||||||
onConfirm: () => dispatch(muteAccount(account.get('id'))),
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onMuteConversation (status) {
|
onMuteConversation (status) {
|
||||||
|
|
|
@ -7,10 +7,10 @@ import {
|
||||||
unfollowAccount,
|
unfollowAccount,
|
||||||
blockAccount,
|
blockAccount,
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
muteAccount,
|
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
} from '../../../actions/accounts';
|
} from '../../../actions/accounts';
|
||||||
import { mentionCompose } from '../../../actions/compose';
|
import { mentionCompose } from '../../../actions/compose';
|
||||||
|
import { initMuteModal } from '../../../actions/mutes';
|
||||||
import { initReport } from '../../../actions/reports';
|
import { initReport } from '../../../actions/reports';
|
||||||
import { openModal } from '../../../actions/modal';
|
import { openModal } from '../../../actions/modal';
|
||||||
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
import { blockDomain, unblockDomain } from '../../../actions/domain_blocks';
|
||||||
|
@ -20,7 +20,6 @@ import { unfollowModal } from '../../../initial_state';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||||
muteConfirm: { id: 'confirmations.mute.confirm', defaultMessage: 'Mute' },
|
|
||||||
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -76,11 +75,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
if (account.getIn(['relationship', 'muting'])) {
|
if (account.getIn(['relationship', 'muting'])) {
|
||||||
dispatch(unmuteAccount(account.get('id')));
|
dispatch(unmuteAccount(account.get('id')));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(initMuteModal(account));
|
||||||
message: <FormattedMessage id='confirmations.mute.message' defaultMessage='Are you sure you want to mute {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
|
|
||||||
confirm: intl.formatMessage(messages.muteConfirm),
|
|
||||||
onConfirm: () => dispatch(muteAccount(account.get('id'))),
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import BoostModal from './boost_modal';
|
||||||
import ConfirmationModal from './confirmation_modal';
|
import ConfirmationModal from './confirmation_modal';
|
||||||
import {
|
import {
|
||||||
OnboardingModal,
|
OnboardingModal,
|
||||||
|
MuteModal,
|
||||||
ReportModal,
|
ReportModal,
|
||||||
EmbedModal,
|
EmbedModal,
|
||||||
} from '../../../features/ui/util/async-components';
|
} from '../../../features/ui/util/async-components';
|
||||||
|
@ -20,6 +21,7 @@ const MODAL_COMPONENTS = {
|
||||||
'VIDEO': () => Promise.resolve({ default: VideoModal }),
|
'VIDEO': () => Promise.resolve({ default: VideoModal }),
|
||||||
'BOOST': () => Promise.resolve({ default: BoostModal }),
|
'BOOST': () => Promise.resolve({ default: BoostModal }),
|
||||||
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
'CONFIRM': () => Promise.resolve({ default: ConfirmationModal }),
|
||||||
|
'MUTE': MuteModal,
|
||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
'EMBED': EmbedModal,
|
'EMBED': EmbedModal,
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
import Button from '../../../components/button';
|
||||||
|
import { closeModal } from '../../../actions/modal';
|
||||||
|
import { muteAccount } from '../../../actions/accounts';
|
||||||
|
import { toggleHideNotifications } from '../../../actions/mutes';
|
||||||
|
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return {
|
||||||
|
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
||||||
|
account: state.getIn(['mutes', 'new', 'account']),
|
||||||
|
notifications: state.getIn(['mutes', 'new', 'notifications']),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapDispatchToProps = dispatch => {
|
||||||
|
return {
|
||||||
|
onConfirm(account, notifications) {
|
||||||
|
dispatch(muteAccount(account.get('id'), notifications));
|
||||||
|
},
|
||||||
|
|
||||||
|
onClose() {
|
||||||
|
dispatch(closeModal());
|
||||||
|
},
|
||||||
|
|
||||||
|
onToggleNotifications() {
|
||||||
|
dispatch(toggleHideNotifications());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
@connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
@injectIntl
|
||||||
|
export default class MuteModal extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
isSubmitting: PropTypes.bool.isRequired,
|
||||||
|
account: PropTypes.object.isRequired,
|
||||||
|
notifications: PropTypes.bool.isRequired,
|
||||||
|
onClose: PropTypes.func.isRequired,
|
||||||
|
onConfirm: PropTypes.func.isRequired,
|
||||||
|
onToggleNotifications: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.button.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onClose();
|
||||||
|
this.props.onConfirm(this.props.account, this.props.notifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCancel = () => {
|
||||||
|
this.props.onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.button = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleNotifications = () => {
|
||||||
|
this.props.onToggleNotifications();
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { account, notifications } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal mute-modal'>
|
||||||
|
<div className='mute-modal__container'>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.mute.message'
|
||||||
|
defaultMessage='Are you sure you want to mute {name}?'
|
||||||
|
values={{ name: <strong>@{account.get('acct')}</strong> }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<label htmlFor='mute-modal__hide-notifications-checkbox'>
|
||||||
|
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
|
||||||
|
{' '}
|
||||||
|
<Toggle id='mute-modal__hide-notifications-checkbox' checked={notifications} onChange={this.toggleNotifications} />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mute-modal__action-bar'>
|
||||||
|
<Button onClick={this.handleCancel} className='mute-modal__cancel-button'>
|
||||||
|
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||||
|
</Button>
|
||||||
|
<Button onClick={this.handleClick} ref={this.setRef}>
|
||||||
|
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -86,6 +86,10 @@ export function OnboardingModal () {
|
||||||
return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal');
|
return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MuteModal () {
|
||||||
|
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
|
||||||
|
}
|
||||||
|
|
||||||
export function ReportModal () {
|
export function ReportModal () {
|
||||||
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import settings from './settings';
|
||||||
import push_notifications from './push_notifications';
|
import push_notifications from './push_notifications';
|
||||||
import status_lists from './status_lists';
|
import status_lists from './status_lists';
|
||||||
import cards from './cards';
|
import cards from './cards';
|
||||||
|
import mutes from './mutes';
|
||||||
import reports from './reports';
|
import reports from './reports';
|
||||||
import contexts from './contexts';
|
import contexts from './contexts';
|
||||||
import compose from './compose';
|
import compose from './compose';
|
||||||
|
@ -37,6 +38,7 @@ const reducers = {
|
||||||
settings,
|
settings,
|
||||||
push_notifications,
|
push_notifications,
|
||||||
cards,
|
cards,
|
||||||
|
mutes,
|
||||||
reports,
|
reports,
|
||||||
contexts,
|
contexts,
|
||||||
compose,
|
compose,
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import Immutable from 'immutable';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MUTES_INIT_MODAL,
|
||||||
|
MUTES_TOGGLE_HIDE_NOTIFICATIONS,
|
||||||
|
} from '../actions/mutes';
|
||||||
|
|
||||||
|
const initialState = Immutable.Map({
|
||||||
|
new: Immutable.Map({
|
||||||
|
isSubmitting: false,
|
||||||
|
account: null,
|
||||||
|
notifications: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function mutes(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case MUTES_INIT_MODAL:
|
||||||
|
return state.withMutations((state) => {
|
||||||
|
state.setIn(['new', 'isSubmitting'], false);
|
||||||
|
state.setIn(['new', 'account'], action.account);
|
||||||
|
state.setIn(['new', 'notifications'], true);
|
||||||
|
});
|
||||||
|
case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
|
||||||
|
return state.updateIn(['new', 'notifications'], (old) => !old);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -906,6 +906,7 @@
|
||||||
.account__relationship {
|
.account__relationship {
|
||||||
height: 18px;
|
height: 18px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.account__header {
|
.account__header {
|
||||||
|
@ -3515,7 +3516,8 @@ button.icon-button.active i.fa-retweet {
|
||||||
.boost-modal,
|
.boost-modal,
|
||||||
.confirmation-modal,
|
.confirmation-modal,
|
||||||
.report-modal,
|
.report-modal,
|
||||||
.actions-modal {
|
.actions-modal,
|
||||||
|
.mute-modal {
|
||||||
background: lighten($ui-secondary-color, 8%);
|
background: lighten($ui-secondary-color, 8%);
|
||||||
color: $ui-base-color;
|
color: $ui-base-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
@ -3565,6 +3567,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
|
|
||||||
.boost-modal__action-bar,
|
.boost-modal__action-bar,
|
||||||
.confirmation-modal__action-bar,
|
.confirmation-modal__action-bar,
|
||||||
|
.mute-modal__action-bar,
|
||||||
.report-modal__action-bar {
|
.report-modal__action-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -3601,6 +3604,14 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mute-modal {
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mute-modal .react-toggle {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.report-modal__statuses,
|
.report-modal__statuses,
|
||||||
.report-modal__comment {
|
.report-modal__comment {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
@ -3673,8 +3684,10 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-modal__action-bar {
|
.confirmation-modal__action-bar,
|
||||||
.confirmation-modal__cancel-button {
|
.mute-modal__action-bar {
|
||||||
|
.confirmation-modal__cancel-button,
|
||||||
|
.mute-modal__cancel-button {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
color: darken($ui-secondary-color, 34%);
|
color: darken($ui-secondary-color, 34%);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -3689,6 +3702,7 @@ button.icon-button.active i.fa-retweet {
|
||||||
}
|
}
|
||||||
|
|
||||||
.confirmation-modal__container,
|
.confirmation-modal__container,
|
||||||
|
.mute-modal__container,
|
||||||
.report-modal__target {
|
.report-modal__target {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
|
@ -17,7 +17,11 @@ module AccountInteractions
|
||||||
end
|
end
|
||||||
|
|
||||||
def muting_map(target_account_ids, account_id)
|
def muting_map(target_account_ids, account_id)
|
||||||
follow_mapping(Mute.where(target_account_id: target_account_ids, account_id: account_id), :target_account_id)
|
Mute.where(target_account_id: target_account_ids, account_id: account_id).each_with_object({}) do |mute, mapping|
|
||||||
|
mapping[mute.target_account_id] = {
|
||||||
|
notifications: mute.hide_notifications?,
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def requested_map(target_account_ids, account_id)
|
def requested_map(target_account_ids, account_id)
|
||||||
|
@ -70,8 +74,13 @@ module AccountInteractions
|
||||||
block_relationships.find_or_create_by!(target_account: other_account)
|
block_relationships.find_or_create_by!(target_account: other_account)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mute!(other_account)
|
def mute!(other_account, notifications: nil)
|
||||||
mute_relationships.find_or_create_by!(target_account: other_account)
|
notifications = true if notifications.nil?
|
||||||
|
mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account)
|
||||||
|
# When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't.
|
||||||
|
if mute.hide_notifications? != notifications
|
||||||
|
mute.update!(hide_notifications: notifications)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def mute_conversation!(conversation)
|
def mute_conversation!(conversation)
|
||||||
|
@ -127,6 +136,10 @@ module AccountInteractions
|
||||||
conversation_mutes.where(conversation: conversation).exists?
|
conversation_mutes.where(conversation: conversation).exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def muting_notifications?(other_account)
|
||||||
|
mute_relationships.where(target_account: other_account, hide_notifications: true).exists?
|
||||||
|
end
|
||||||
|
|
||||||
def requested?(other_account)
|
def requested?(other_account)
|
||||||
follow_requests.where(target_account: other_account).exists?
|
follow_requests.where(target_account: other_account).exists?
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,12 @@
|
||||||
#
|
#
|
||||||
# Table name: mutes
|
# Table name: mutes
|
||||||
#
|
#
|
||||||
# created_at :datetime not null
|
# id :integer not null, primary key
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
# account_id :bigint not null
|
# updated_at :datetime not null
|
||||||
# id :bigint not null, primary key
|
# account_id :integer not null
|
||||||
# target_account_id :bigint not null
|
# target_account_id :integer not null
|
||||||
|
# hide_notifications :boolean default(TRUE), not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class Mute < ApplicationRecord
|
class Mute < ApplicationRecord
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class MuteService < BaseService
|
class MuteService < BaseService
|
||||||
def call(account, target_account)
|
def call(account, target_account, notifications: nil)
|
||||||
return if account.id == target_account.id
|
return if account.id == target_account.id
|
||||||
mute = account.mute!(target_account)
|
FeedManager.instance.clear_from_timeline(account, target_account)
|
||||||
|
mute = account.mute!(target_account, notifications: notifications)
|
||||||
BlockWorker.perform_async(account.id, target_account.id)
|
BlockWorker.perform_async(account.id, target_account.id)
|
||||||
mute
|
mute
|
||||||
end
|
end
|
||||||
|
|
|
@ -81,7 +81,7 @@ class NotifyService < BaseService
|
||||||
blocked ||= from_self? # Skip for interactions with self
|
blocked ||= from_self? # Skip for interactions with self
|
||||||
blocked ||= domain_blocking? # Skip for domain blocked accounts
|
blocked ||= domain_blocking? # Skip for domain blocked accounts
|
||||||
blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts
|
blocked ||= @recipient.blocking?(@notification.from_account) # Skip for blocked accounts
|
||||||
blocked ||= @recipient.muting?(@notification.from_account) # Skip for muted accounts
|
blocked ||= @recipient.muting_notifications?(@notification.from_account)
|
||||||
blocked ||= hellbanned? # Hellban
|
blocked ||= hellbanned? # Hellban
|
||||||
blocked ||= optional_non_follower? # Options
|
blocked ||= optional_non_follower? # Options
|
||||||
blocked ||= optional_non_following? # Options
|
blocked ||= optional_non_following? # Options
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
require Rails.root.join('lib', 'mastodon', 'migration_helpers')
|
||||||
|
|
||||||
|
class AddHideNotificationsToMute < ActiveRecord::Migration[5.1]
|
||||||
|
include Mastodon::MigrationHelpers
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_column_with_default :mutes, :hide_notifications, :boolean, default: true, allow_null: false
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_column :mutes, :hide_notifications
|
||||||
|
end
|
||||||
|
end
|
|
@ -203,6 +203,7 @@ ActiveRecord::Schema.define(version: 20171114080328) do
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.bigint "account_id", null: false
|
t.bigint "account_id", null: false
|
||||||
t.bigint "target_account_id", null: false
|
t.bigint "target_account_id", null: false
|
||||||
|
t.boolean "hide_notifications", default: true, null: false
|
||||||
t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true
|
t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -137,6 +137,35 @@ RSpec.describe Api::V1::AccountsController, type: :controller do
|
||||||
it 'creates a muting relation' do
|
it 'creates a muting relation' do
|
||||||
expect(user.account.muting?(other_account)).to be true
|
expect(user.account.muting?(other_account)).to be true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'mutes notifications' do
|
||||||
|
expect(user.account.muting_notifications?(other_account)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'POST #mute with notifications set to false' do
|
||||||
|
let(:other_account) { Fabricate(:user, email: 'bob@example.com', account: Fabricate(:account, username: 'bob')).account }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.follow!(other_account)
|
||||||
|
post :mute, params: {id: other_account.id, notifications: false }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http success' do
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not remove the following relation between user and target user' do
|
||||||
|
expect(user.account.following?(other_account)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates a muting relation' do
|
||||||
|
expect(user.account.muting?(other_account)).to be true
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mute notifications' do
|
||||||
|
expect(user.account.muting_notifications?(other_account)).to be false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST #unmute' do
|
describe 'POST #unmute' do
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Api::V1::MutesController, type: :controller do
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'follow') }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
Fabricate(:mute, account: user.account)
|
Fabricate(:mute, account: user.account, hide_notifications: false)
|
||||||
allow(controller).to receive(:doorkeeper_token) { token }
|
allow(controller).to receive(:doorkeeper_token) { token }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe AccountInteractions do
|
||||||
|
describe 'muting an account' do
|
||||||
|
let(:me) { Fabricate(:account, username: 'Me') }
|
||||||
|
let(:you) { Fabricate(:account, username: 'You') }
|
||||||
|
|
||||||
|
context 'with the notifications option unspecified' do
|
||||||
|
before do
|
||||||
|
me.mute!(you)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults to muting notifications' do
|
||||||
|
expect(me.muting_notifications?(you)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the notifications option set to false' do
|
||||||
|
before do
|
||||||
|
me.mute!(you, notifications: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mute notifications' do
|
||||||
|
expect(me.muting_notifications?(you)).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with the notifications option set to true' do
|
||||||
|
before do
|
||||||
|
me.mute!(you, notifications: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does mute notifications' do
|
||||||
|
expect(me.muting_notifications?(you)).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -32,4 +32,36 @@ RSpec.describe MuteService do
|
||||||
account.muting?(target_account)
|
account.muting?(target_account)
|
||||||
}.from(false).to(true)
|
}.from(false).to(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'without specifying a notifications parameter' do
|
||||||
|
it 'mutes notifications from the account' do
|
||||||
|
is_expected.to change {
|
||||||
|
account.muting_notifications?(target_account)
|
||||||
|
}.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a true notifications parameter' do
|
||||||
|
subject do
|
||||||
|
-> { described_class.new.call(account, target_account, notifications: true) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'mutes notifications from the account' do
|
||||||
|
is_expected.to change {
|
||||||
|
account.muting_notifications?(target_account)
|
||||||
|
}.from(false).to(true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a false notifications parameter' do
|
||||||
|
subject do
|
||||||
|
-> { described_class.new.call(account, target_account, notifications: false) }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not mute notifications from the account' do
|
||||||
|
is_expected.to_not change {
|
||||||
|
account.muting_notifications?(target_account)
|
||||||
|
}.from(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,6 +17,16 @@ RSpec.describe NotifyService do
|
||||||
is_expected.to_not change(Notification, :count)
|
is_expected.to_not change(Notification, :count)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not notify when sender is muted with hide_notifications' do
|
||||||
|
recipient.mute!(sender, notifications: true)
|
||||||
|
is_expected.to_not change(Notification, :count)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does notify when sender is muted without hide_notifications' do
|
||||||
|
recipient.mute!(sender, notifications: false)
|
||||||
|
is_expected.to change(Notification, :count)
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not notify when sender\'s domain is blocked' do
|
it 'does not notify when sender\'s domain is blocked' do
|
||||||
recipient.block_domain!(sender.domain)
|
recipient.block_domain!(sender.domain)
|
||||||
is_expected.to_not change(Notification, :count)
|
is_expected.to_not change(Notification, :count)
|
||||||
|
|
Reference in New Issue