Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow notification filter policy to also mute notifications #2799

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/controllers/api/v1/notifications/policies_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ def resource_params
:filter_not_following,
:filter_not_followers,
:filter_new_accounts,
:filter_private_mentions
:filter_private_mentions,
:mute_not_following,
:mute_not_followers,
:mute_new_accounts,
:mute_private_mentions
)
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export interface NotificationPolicyJSON {
filter_not_followers: boolean;
filter_new_accounts: boolean;
filter_private_mentions: boolean;
mute_not_following: boolean;
mute_not_followers: boolean;
mute_new_accounts: boolean;
mute_private_mentions: boolean;
summary: {
pending_requests_count: number;
pending_notifications_count: number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'flavours/glitch/permissions';

import { CheckboxWithLabel } from './checkbox_with_label';
import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
import PillBarButton from './pill_bar_button';
import PillBarToggle from './pill_bar_toggle';
import SettingToggle from './setting_toggle';

class ColumnSettings extends PureComponent {
Expand Down Expand Up @@ -49,6 +49,22 @@ class ColumnSettings extends PureComponent {
this.props.onChangePolicy('filter_private_mentions', checked);
};

handleMuteNotFollowing = checked => {
this.props.onChangePolicy('mute_not_following', checked);
};

handleMuteNotFollowers = checked => {
this.props.onChangePolicy('mute_not_followers', checked);
};

handleMuteNewAccounts = checked => {
this.props.onChangePolicy('mute_new_accounts', checked);
};

handleMutePrivateMentions = checked => {
this.props.onChangePolicy('mute_private_mentions', checked);
};

render () {
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;

Expand All @@ -59,6 +75,8 @@ class ColumnSettings extends PureComponent {
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
const filterStr = <FormattedMessage id='simple_form.labels.filters.actions.warn' defaultMessage='Hide with a warning' />;
const muteStr = <FormattedMessage id='simple_form.labels.filters.actions.hide' defaultMessage='Hide completely' />;

const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
Expand All @@ -85,31 +103,55 @@ class ColumnSettings extends PureComponent {
<h3><FormattedMessage id='notifications.policy.title' defaultMessage='Filter out notifications from…' /></h3>

<div className='column-settings__row'>
<CheckboxWithLabel checked={notificationPolicy.filter_not_following} onChange={this.handleFilterNotFollowing}>
<strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
</CheckboxWithLabel>

<CheckboxWithLabel checked={notificationPolicy.filter_not_followers} onChange={this.handleFilterNotFollowers}>
<strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_followers_hint' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' values={{ days: 3 }} /></span>
</CheckboxWithLabel>

<CheckboxWithLabel checked={notificationPolicy.filter_new_accounts} onChange={this.handleFilterNewAccounts}>
<strong><FormattedMessage id='notifications.policy.filter_new_accounts_title' defaultMessage='New accounts' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_new_accounts.hint' defaultMessage='Created within the past {days, plural, one {one day} other {# days}}' values={{ days: 30 }} /></span>
</CheckboxWithLabel>

<CheckboxWithLabel checked={notificationPolicy.filter_private_mentions} onChange={this.handleFilterPrivateMentions}>
<strong><FormattedMessage id='notifications.policy.filter_private_mentions_title' defaultMessage='Unsolicited private mentions' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_private_mentions_hint' defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" /></span>
</CheckboxWithLabel>
<div aria-labelledby='notifications.policy.filter_not_following_title'>
<div className='app-form__toggle__label pillbar'>
<strong><FormattedMessage id='notifications.policy.filter_not_following_title' defaultMessage="People you don't follow" /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_following_hint' defaultMessage='Until you manually approve them' /></span>
</div>
<div className='column-settings__pillbar'>
<PillBarToggle id={'filter-not-following'} active={notificationPolicy.filter_not_following} label={filterStr} onChange={this.handleFilterNotFollowing} />
<PillBarToggle id={'mute-not-following'} active={notificationPolicy.mute_not_following} label={muteStr} onChange={this.handleMuteNotFollowing} />
</div>
</div>

<div aria-labelledby='notifications.policy.filter_not_followers_title'>
<div className='app-form__toggle__label pillbar'>
<strong><FormattedMessage id='notifications.policy.filter_not_followers_title' defaultMessage='People not following you' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_not_followers_hint' defaultMessage='Including people who have been following you fewer than {days, plural, one {one day} other {# days}}' values={{ days: 3 }} /></span>
</div>
<div className='column-settings__pillbar'>
<PillBarToggle id={'filter-not-followers'} active={notificationPolicy.filter_not_followers} label={filterStr} onChange={this.handleFilterNotFollowers} />
<PillBarToggle id={'mute-not-followers'} active={notificationPolicy.mute_not_followers} label={muteStr} onChange={this.handleMuteNotFollowers} />
</div>
</div>

<div aria-labelledby='notifications.policy.filter_new_accounts_title'>
<div className='app-form__toggle__label pillbar'>
<strong><FormattedMessage id='notifications.policy.filter_new_accounts_title' defaultMessage='New accounts' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_new_accounts.hint' defaultMessage='Created within the past {days, plural, one {one day} other {# days}}' values={{ days: 30 }} /></span>
</div>
<div className='column-settings__pillbar'>
<PillBarToggle id={'filter-new-accounts'} active={notificationPolicy.filter_new_accounts} label={filterStr} onChange={this.handleFilterNewAccounts} />
<PillBarToggle id={'mute-new-accounts'} active={notificationPolicy.mute_new_accounts} label={muteStr} onChange={this.handleMuteNewAccounts} />
</div>
</div>

<div aria-labelledby='notifications.policy.filter_private_mentions_title'>
<div className='app-form__toggle__label pillbar'>
<strong><FormattedMessage id='notifications.policy.filter_private_mentions_title' defaultMessage='Unsolicited private mentions' /></strong>
<span className='hint'><FormattedMessage id='notifications.policy.filter_private_mentions_hint' defaultMessage="Filtered unless it's in reply to your own mention or if you follow the sender" /></span>
</div>
<div className='column-settings__pillbar'>
<PillBarToggle id={'filter-private-mentions'} active={notificationPolicy.filter_private_mentions} label={filterStr} onChange={this.handleFilterPrivateMentions} />
<PillBarToggle id={'mute-private-mentions'} active={notificationPolicy.mute_private_mentions} label={muteStr} onChange={this.handleMutePrivateMentions} />
</div>
</div>
</div>
</section>

<section role='group' aria-labelledby='notifications-filter-bar'>
<h3 id='notifications-filter-bar'><FormattedMessage id='notifications.column_settings.filter_bar.category' defaultMessage='Quick filter bar' /></h3>

<div className='column-settings__row'>
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'show']} onChange={onChange} label={filterBarShowStr} />
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['quickFilter', 'advanced']} onChange={onChange} label={filterAdvancedStr} />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';

import classNames from 'classnames';

export default class PillBarToggle extends PureComponent {

static propTypes = {
id: PropTypes.string.isRequired,
active: PropTypes.bool.isRequired,
label: PropTypes.node.isRequired,
onChange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
};

onChange = () => {
this.props.onChange(!this.props.active);
};

render () {
const { id, active, label, disabled } = this.props;
const prop_id = ['setting-pillbar-button', id].filter(Boolean).join('-');

return (
<button
key={prop_id}
id={prop_id}
className={classNames('pillbar-button', { active })}
disabled={disabled}
onClick={this.onChange}
aria-pressed={active}
>
{label}
</button>
);
}
}
5 changes: 5 additions & 0 deletions app/javascript/flavours/glitch/styles/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,11 @@ code {
margin: 0 4px;
margin-top: -2px;
}

// Extra bottom margin when used as a label over a pillbar button group
&.pillbar {
margin-bottom: 0.5em;
}
}

&__toggle {
Expand Down
32 changes: 32 additions & 0 deletions app/models/notification_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
# filter_private_mentions :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
# mute_not_following :boolean default(FALSE), not null
# mute_not_followers :boolean default(FALSE), not null
# mute_new_accounts :boolean default(FALSE), not null
# mute_private_mentions :boolean default(FALSE), not null
#

class NotificationPolicy < ApplicationRecord
Expand All @@ -21,6 +25,8 @@ class NotificationPolicy < ApplicationRecord

attr_reader :pending_requests_count, :pending_notifications_count

before_update :ensure_filter_mute_exclusivity

MAX_MEANINGFUL_COUNT = 100

def summarize!
Expand All @@ -33,4 +39,30 @@ def summarize!
def pending_notification_requests
@pending_notification_requests ||= notification_requests.limit(MAX_MEANINGFUL_COUNT).pick(Arel.sql('count(*), coalesce(sum(notifications_count), 0)::bigint'))
end

def ensure_filter_mute_exclusivity
if filter_not_following_changed? && filter_not_following?
self.mute_not_following = false
elsif mute_not_following_changed? && mute_not_following?
self.filter_not_following = false
end

if filter_not_followers_changed? && filter_not_followers?
self.mute_not_followers = false
elsif mute_not_followers_changed? && mute_not_followers?
self.filter_not_followers = false
end

if filter_new_accounts_changed? && filter_new_accounts?
self.mute_new_accounts = false
elsif mute_new_accounts_changed? && mute_new_accounts?
self.filter_new_accounts = false
end

if filter_private_mentions_changed? && filter_private_mentions?
self.mute_private_mentions = false
elsif mute_private_mentions_changed? && mute_private_mentions?
self.filter_private_mentions = false
end
end
end
4 changes: 4 additions & 0 deletions app/serializers/rest/notification_policy_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class REST::NotificationPolicySerializer < ActiveModel::Serializer
:filter_not_followers,
:filter_new_accounts,
:filter_private_mentions,
:mute_not_following,
:mute_not_followers,
:mute_new_accounts,
:mute_private_mentions,
:summary

def summary
Expand Down
28 changes: 27 additions & 1 deletion app/services/notify_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def dismiss?
blocked ||= @recipient.muting_notifications?(@sender)
blocked ||= conversation_muted?
blocked ||= blocked_mention? if message?
blocked
blocked || FilterCondition.new(@notification).dismiss?
end

private
Expand Down Expand Up @@ -99,6 +99,16 @@ def filter?
filtered_by_private_mentions_policy?
end

def dismiss?
return false unless Notification::PROPERTIES[@notification.type][:filterable]
return false if override_for_sender?

muted_by_not_following_policy? ||
muted_by_not_followers_policy? ||
muted_by_new_accounts_policy? ||
muted_by_private_mentions_policy?
end

private

def filtered_by_not_following_policy?
Expand All @@ -117,6 +127,22 @@ def filtered_by_private_mentions_policy?
@policy.filter_private_mentions? && not_following? && private_mention_not_in_response?
end

def muted_by_not_following_policy?
@policy.mute_not_following? && not_following?
end

def muted_by_not_followers_policy?
@policy.mute_not_followers? && not_follower?
end

def muted_by_new_accounts_policy?
@policy.mute_new_accounts? && new_account?
end

def muted_by_private_mentions_policy?
@policy.mute_private_mentions? && not_following? && private_mention_not_in_response?
end

def not_following?
[email protected]?(@sender)
end
Expand Down
12 changes: 12 additions & 0 deletions db/migrate/20240729030729_add_mute_to_notification_policies.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class AddMuteToNotificationPolicies < ActiveRecord::Migration[7.1]
disable_ddl_transaction!

def change
add_column :notification_policies, :mute_not_following, :boolean, default: false, null: false
add_column :notification_policies, :mute_not_followers, :boolean, default: false, null: false
add_column :notification_policies, :mute_new_accounts, :boolean, default: false, null: false
add_column :notification_policies, :mute_private_mentions, :boolean, default: false, null: false
end
end
6 changes: 5 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2024_07_24_181224) do
ActiveRecord::Schema[7.1].define(version: 2024_07_29_030729) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

Expand Down Expand Up @@ -698,6 +698,10 @@
t.boolean "filter_private_mentions", default: true, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "mute_not_following", default: false, null: false
t.boolean "mute_not_followers", default: false, null: false
t.boolean "mute_new_accounts", default: false, null: false
t.boolean "mute_private_mentions", default: false, null: false
t.index ["account_id"], name: "index_notification_policies_on_account_id", unique: true
end

Expand Down