Further de-emphasize filtered notifications banner and add setting to minimize it (#31250)

This commit is contained in:
Claire 2024-08-02 16:59:37 +02:00 committed by GitHub
parent 2ec1181ee5
commit ad95c98054
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 321 additions and 119 deletions

View file

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import { useCallback } from 'react';
import Toggle from 'react-toggle';
export const CheckboxWithLabel = ({ checked, disabled, children, onChange }) => {
const handleChange = useCallback(({ target }) => {
onChange(target.checked);
}, [onChange]);
return (
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>
{children}
</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle checked={checked} onChange={handleChange} disabled={disabled} />
</div>
</div>
</label>
);
};
CheckboxWithLabel.propTypes = {
checked: PropTypes.bool,
disabled: PropTypes.bool,
children: PropTypes.children,
onChange: PropTypes.func,
};

View file

@ -0,0 +1,40 @@
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
import Toggle from 'react-toggle';
interface Props {
checked: boolean;
disabled?: boolean;
onChange: (checked: boolean) => void;
}
export const CheckboxWithLabel: React.FC<PropsWithChildren<Props>> = ({
checked,
disabled,
children,
onChange,
}) => {
const handleChange = useCallback(
({ target }: React.ChangeEvent<HTMLInputElement>) => {
onChange(target.checked);
},
[onChange],
);
return (
<label className='app-form__toggle'>
<div className='app-form__toggle__label'>{children}</div>
<div className='app-form__toggle__toggle'>
<div>
<Toggle
checked={checked}
onChange={handleChange}
disabled={disabled}
/>
</div>
</div>
</label>
);
};

View file

@ -8,9 +8,9 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'mastodon/permissions';
import { CheckboxWithLabel } from './checkbox_with_label';
import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
import { PolicyControls } from './policy_controls';
import SettingToggle from './setting_toggle';
class ColumnSettings extends PureComponent {
@ -24,32 +24,14 @@ class ColumnSettings extends PureComponent {
alertsEnabled: PropTypes.bool,
browserSupport: PropTypes.bool,
browserPermission: PropTypes.string,
notificationPolicy: PropTypes.object.isRequired,
onChangePolicy: PropTypes.func.isRequired,
};
onPushChange = (path, checked) => {
this.props.onChange(['push', ...path], checked);
};
handleFilterNotFollowing = checked => {
this.props.onChangePolicy('filter_not_following', checked);
};
handleFilterNotFollowers = checked => {
this.props.onChangePolicy('filter_not_followers', checked);
};
handleFilterNewAccounts = checked => {
this.props.onChangePolicy('filter_new_accounts', checked);
};
handleFilterPrivateMentions = checked => {
this.props.onChangePolicy('filter_private_mentions', checked);
};
render () {
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission, notificationPolicy } = this.props;
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
const unreadMarkersShowStr = <FormattedMessage id='notifications.column_settings.unread_notifications.highlight' defaultMessage='Highlight unread notifications' />;
@ -79,31 +61,7 @@ class ColumnSettings extends PureComponent {
</section>
)}
<section>
<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>
</section>
<PolicyControls />
<section role='group' aria-labelledby='notifications-beta'>
<h3 id='notifications-beta'>

View file

@ -1,18 +1,62 @@
import { useEffect } from 'react';
import { useCallback, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { Link } from 'react-router-dom';
import { Link, useHistory } from 'react-router-dom';
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import { fetchNotificationPolicy } from 'mastodon/actions/notification_policies';
import { Icon } from 'mastodon/components/icon';
import { selectSettingsNotificationsMinimizeFilteredBanner } from 'mastodon/selectors/settings';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { toCappedNumber } from 'mastodon/utils/numbers';
const messages = defineMessages({
filteredNotifications: {
id: 'notification_requests.title',
defaultMessage: 'Filtered notifications',
},
});
export const FilteredNotificationsIconButton: React.FC<{
className?: string;
}> = ({ className }) => {
const intl = useIntl();
const history = useHistory();
const policy = useAppSelector((state) => state.notificationPolicy);
const minimizeSetting = useAppSelector(
selectSettingsNotificationsMinimizeFilteredBanner,
);
const handleClick = useCallback(() => {
history.push('/notifications/requests');
}, [history]);
if (policy === null || policy.summary.pending_notifications_count === 0) {
return null;
}
if (!minimizeSetting) {
return null;
}
return (
<button
aria-label={intl.formatMessage(messages.filteredNotifications)}
title={intl.formatMessage(messages.filteredNotifications)}
onClick={handleClick}
className={className}
>
<Icon id='filtered-notifications' icon={InventoryIcon} />
</button>
);
};
export const FilteredNotificationsBanner: React.FC = () => {
const dispatch = useAppDispatch();
const policy = useAppSelector((state) => state.notificationPolicy);
const minimizeSetting = useAppSelector(
selectSettingsNotificationsMinimizeFilteredBanner,
);
useEffect(() => {
void dispatch(fetchNotificationPolicy());
@ -30,6 +74,10 @@ export const FilteredNotificationsBanner: React.FC = () => {
return null;
}
if (minimizeSetting) {
return null;
}
return (
<Link
className='filtered-notifications-banner'
@ -54,10 +102,6 @@ export const FilteredNotificationsBanner: React.FC = () => {
/>
</span>
</div>
<div className='filtered-notifications-banner__badge'>
{toCappedNumber(policy.summary.pending_notifications_count)}
</div>
</Link>
);
};

View file

@ -0,0 +1,141 @@
import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { updateNotificationsPolicy } from 'mastodon/actions/notification_policies';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { CheckboxWithLabel } from './checkbox_with_label';
export const PolicyControls: React.FC = () => {
const dispatch = useAppDispatch();
const notificationPolicy = useAppSelector(
(state) => state.notificationPolicy,
);
const handleFilterNotFollowing = useCallback(
(checked: boolean) => {
void dispatch(
updateNotificationsPolicy({ filter_not_following: checked }),
);
},
[dispatch],
);
const handleFilterNotFollowers = useCallback(
(checked: boolean) => {
void dispatch(
updateNotificationsPolicy({ filter_not_followers: checked }),
);
},
[dispatch],
);
const handleFilterNewAccounts = useCallback(
(checked: boolean) => {
void dispatch(
updateNotificationsPolicy({ filter_new_accounts: checked }),
);
},
[dispatch],
);
const handleFilterPrivateMentions = useCallback(
(checked: boolean) => {
void dispatch(
updateNotificationsPolicy({ filter_private_mentions: checked }),
);
},
[dispatch],
);
if (!notificationPolicy) return null;
return (
<section>
<h3>
<FormattedMessage
id='notifications.policy.title'
defaultMessage='Filter out notifications from…'
/>
</h3>
<div className='column-settings__row'>
<CheckboxWithLabel
checked={notificationPolicy.filter_not_following}
onChange={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={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={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={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>
</section>
);
};

View file

@ -6,7 +6,6 @@ import { openModal } from 'mastodon/actions/modal';
import { initializeNotifications } from 'mastodon/actions/notifications_migration';
import { showAlert } from '../../../actions/alerts';
import { updateNotificationsPolicy } from '../../../actions/notification_policies';
import { setFilter, requestBrowserPermission } from '../../../actions/notifications';
import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications';
import { changeSetting } from '../../../actions/settings';
@ -25,7 +24,6 @@ const mapStateToProps = state => ({
alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true),
browserSupport: state.getIn(['notifications', 'browserSupport']),
browserPermission: state.getIn(['notifications', 'browserPermission']),
notificationPolicy: state.notificationPolicy,
});
const mapDispatchToProps = (dispatch) => ({
@ -74,12 +72,6 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(requestBrowserPermission());
},
onChangePolicy (param, checked) {
dispatch(updateNotificationsPolicy({
[param]: checked,
}));
},
});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings));

View file

@ -34,7 +34,10 @@ import ColumnHeader from '../../components/column_header';
import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list';
import { FilteredNotificationsBanner } from './components/filtered_notifications_banner';
import {
FilteredNotificationsBanner,
FilteredNotificationsIconButton,
} from './components/filtered_notifications_banner';
import NotificationsPermissionBanner from './components/notifications_permission_banner';
import ColumnSettingsContainer from './containers/column_settings_container';
import FilterBarContainer from './containers/filter_bar_container';
@ -255,10 +258,10 @@ class Notifications extends PureComponent {
scrollContainer = <NotSignedInIndicator />;
}
let extraButton = null;
if (canMarkAsRead) {
extraButton = (
const extraButton = (
<>
<FilteredNotificationsIconButton className='column-header__button' />
{canMarkAsRead && (
<button
aria-label={intl.formatMessage(messages.markAsRead)}
title={intl.formatMessage(messages.markAsRead)}
@ -267,8 +270,9 @@ class Notifications extends PureComponent {
>
<Icon id='done-all' icon={DoneAllIcon} />
</button>
)}
</>
);
}
return (
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>

View file

@ -9,16 +9,52 @@ import { useSelector, useDispatch } from 'react-redux';
import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
import { fetchNotificationRequests, expandNotificationRequests } from 'mastodon/actions/notifications';
import { changeSetting } from 'mastodon/actions/settings';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import ScrollableList from 'mastodon/components/scrollable_list';
import { NotificationRequest } from './components/notification_request';
import { PolicyControls } from './components/policy_controls';
import SettingToggle from './components/setting_toggle';
const messages = defineMessages({
title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
maximize: { id: 'notification_requests.maximize', defaultMessage: 'Maximize' }
});
const ColumnSettings = () => {
const dispatch = useDispatch();
const settings = useSelector((state) => state.settings.get('notifications'));
const onChange = useCallback(
(key, checked) => {
dispatch(changeSetting(['notifications', ...key], checked));
},
[dispatch],
);
return (
<div className='column-settings'>
<section>
<div className='column-settings__row'>
<SettingToggle
prefix='notifications'
settings={settings}
settingPath={['minimizeFilteredBanner']}
onChange={onChange}
label={
<FormattedMessage id='notification_requests.minimize_banner' defaultMessage='Minimize filtred notifications banner' />
}
/>
</div>
</section>
<PolicyControls />
</div>
);
};
export const NotificationRequests = ({ multiColumn }) => {
const columnRef = useRef();
const intl = useIntl();
@ -48,7 +84,9 @@ export const NotificationRequests = ({ multiColumn }) => {
onClick={handleHeaderClick}
multiColumn={multiColumn}
showBackButton
/>
>
<ColumnSettings />
</ColumnHeader>
<ScrollableList
scrollKey='notification_requests'

View file

@ -43,7 +43,10 @@ import Column from '../../components/column';
import { ColumnHeader } from '../../components/column_header';
import { LoadGap } from '../../components/load_gap';
import ScrollableList from '../../components/scrollable_list';
import { FilteredNotificationsBanner } from '../notifications/components/filtered_notifications_banner';
import {
FilteredNotificationsBanner,
FilteredNotificationsIconButton,
} from '../notifications/components/filtered_notifications_banner';
import NotificationsPermissionBanner from '../notifications/components/notifications_permission_banner';
import ColumnSettingsContainer from '../notifications/containers/column_settings_container';
@ -306,7 +309,10 @@ export const Notifications: React.FC<{
<NotSignedInIndicator />
);
const extraButton = canMarkAsRead ? (
const extraButton = (
<>
<FilteredNotificationsIconButton className='column-header__button' />
{canMarkAsRead && (
<button
aria-label={intl.formatMessage(messages.markAsRead)}
title={intl.formatMessage(messages.markAsRead)}
@ -315,7 +321,9 @@ export const Notifications: React.FC<{
>
<Icon id='done-all' icon={DoneAllIcon} />
</button>
) : null;
)}
</>
);
return (
<Column

View file

@ -505,6 +505,8 @@
"notification.update": "{name} edited a post",
"notification_requests.accept": "Accept",
"notification_requests.dismiss": "Dismiss",
"notification_requests.maximize": "Maximize",
"notification_requests.minimize_banner": "Minimize filtred notifications banner",
"notification_requests.notifications_from": "Notifications from {name}",
"notification_requests.title": "Filtered notifications",
"notifications.clear": "Clear notifications",

View file

@ -51,6 +51,7 @@ const initialState = ImmutableMap({
dismissPermissionBanner: false,
showUnread: true,
minimizeFilteredBanner: false,
shows: ImmutableMap({
follow: true,

View file

@ -37,4 +37,9 @@ export const selectNeedsNotificationPermission = (state: RootState) =>
'dismissPermissionBanner',
])) as boolean;
export const selectSettingsNotificationsMinimizeFilteredBanner = (
state: RootState,
) =>
state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean;
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */