diff --git a/app/javascript/flavours/glitch/actions/notification_groups.ts b/app/javascript/flavours/glitch/actions/notification_groups.ts index b145898494..0e4d291ae0 100644 --- a/app/javascript/flavours/glitch/actions/notification_groups.ts +++ b/app/javascript/flavours/glitch/actions/notification_groups.ts @@ -36,9 +36,18 @@ function notificationTypeForFilter(type: NotificationType) { } function notificationTypeForQuickFilter(type: NotificationType) { - if (type === 'quoted_update') return 'update'; - else if (type === 'quote') return 'mention'; - else return type; + switch (type) { + case 'quoted_update': + return 'update'; + case 'quote': + return 'mention'; + case 'collection_update': + return 'collection'; + case 'added_to_collection': + return 'collection'; + default: + return type; + } } function excludeAllTypesExcept(filter: string) { diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index da0c5f1102..cb4cd1251c 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -86,6 +86,9 @@ export function setupBrowserNotifications() { }; } +/** + * @param {(NotificationPermission) => void} callback + */ export function requestBrowserPermission(callback = noOp) { return dispatch => { requestNotificationPermission((permission) => { diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx index 30999e33e5..58d4f81456 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings.jsx @@ -13,6 +13,7 @@ import GrantPermissionButton from './grant_permission_button'; import PillBarButton from './pill_bar_button'; import { PolicyControls } from './policy_controls'; import SettingToggle from './setting_toggle'; +import { ColumnSettingsGroup } from './column_settings_group'; class ColumnSettings extends PureComponent { static propTypes = { @@ -191,6 +192,11 @@ class ColumnSettings extends PureComponent { + } + /> + {((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && ( diff --git a/app/javascript/flavours/glitch/features/notifications/components/column_settings_group.tsx b/app/javascript/flavours/glitch/features/notifications/components/column_settings_group.tsx new file mode 100644 index 0000000000..bbafac7de7 --- /dev/null +++ b/app/javascript/flavours/glitch/features/notifications/components/column_settings_group.tsx @@ -0,0 +1,162 @@ +import { useCallback } from 'react'; +import type { FC, ReactNode } from 'react'; + +import { defineMessages, FormattedMessage } from 'react-intl'; + +import { showAlert } from '@/flavours/glitch/actions/alerts'; +import { requestBrowserPermission } from '@/flavours/glitch/actions/notifications'; +import { changeAlerts } from '@/flavours/glitch/actions/push_notifications'; +import { changeSetting } from '@/flavours/glitch/actions/settings'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/flavours/glitch/store'; + +import PillBarButton from './pill_bar_button'; + +const selectNotificationSettings = createAppSelector( + [ + (state) => + (state.settings as Immutable.Map).get( + 'notifications', + ) as Immutable.Map | boolean>, + (state) => state.notifications.get('browserPermission') as string, + (state) => state.push_notifications, + ], + (settings, browserPermission, pushSettings) => ({ + settings, + browserPermission: browserPermission !== 'denied', + pushSettings, + showPushSettings: + pushSettings.get('supported') && pushSettings.get('enabled'), + }), +); + +const messages = defineMessages({ + permissionDenied: { + id: 'notifications.permission_denied_alert', + defaultMessage: + "Desktop notifications can't be enabled, as browser permission has been denied before", + }, +}); + +type SettingsType = 'alerts' | 'shows' | 'sounds'; + +export const ColumnSettingsGroup: FC<{ label: ReactNode; type: string }> = ({ + label, + type, +}) => { + const { settings, browserPermission, pushSettings, showPushSettings } = + useAppSelector(selectNotificationSettings); + + const dispatch = useAppDispatch(); + const handleChange = useCallback( + (path: [SettingsType, string], checked: boolean) => { + if ( + path[0] === 'alerts' && + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', ...path], checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeSetting(['notifications', ...path], checked)); + } + }, + [dispatch], + ); + const handlePushChange = useCallback( + (path: string[], checked: boolean) => { + if ( + checked && + typeof window.Notification !== 'undefined' && + Notification.permission !== 'granted' + ) { + dispatch( + requestBrowserPermission((permission: NotificationPermission) => { + if (permission === 'granted') { + dispatch(changeAlerts(path, checked)); + } else { + dispatch(showAlert({ message: messages.permissionDenied })); + } + }), + ); + } else { + dispatch(changeAlerts(path, checked)); + } + }, + [dispatch], + ); + + return ( + + {label} + + + + } + /> + + {showPushSettings && ( + + } + /> + )} + + + } + /> + + + } + /> + + + ); +}; diff --git a/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx b/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx index 84aebef79f..1ddcf0bf74 100644 --- a/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx +++ b/app/javascript/flavours/glitch/features/notifications_v2/filter_bar.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import CollectionsIcon from '@/material-icons/400-24px/category.svg?react'; import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react'; @@ -27,6 +28,10 @@ const tooltips = defineMessages({ boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' }, polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' }, follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' }, + collections: { + id: 'notifications.filter.collections', + defaultMessage: 'Collections', + }, statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow', @@ -130,6 +135,14 @@ export const FilterBar: React.FC = () => { > + + + ); else diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index a9d2953c85..ca52994e1f 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -42,6 +42,7 @@ const initialState = ImmutableMap({ poll: false, status: false, update: false, + collections: false, 'admin.sign_up': false, 'admin.report': false, }), @@ -66,6 +67,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), @@ -80,6 +82,7 @@ const initialState = ImmutableMap({ poll: true, status: true, update: true, + collections: true, 'admin.sign_up': true, 'admin.report': true, }), diff --git a/app/javascript/flavours/glitch/selectors/notifications.ts b/app/javascript/flavours/glitch/selectors/notifications.ts index a27876d6c3..df31fe2ce8 100644 --- a/app/javascript/flavours/glitch/selectors/notifications.ts +++ b/app/javascript/flavours/glitch/selectors/notifications.ts @@ -29,7 +29,10 @@ const filterNotificationsByAllowedTypes = ( (item) => item.type === 'gap' || allowedType === item.type || - (allowedType === 'mention' && item.type === 'quote'), + (allowedType === 'mention' && item.type === 'quote') || + (allowedType === 'collection' && + (item.type === 'collection_update' || + item.type === 'added_to_collection')), ); }; diff --git a/app/javascript/flavours/glitch/selectors/settings.ts b/app/javascript/flavours/glitch/selectors/settings.ts index 5229b15cf0..067ac34a76 100644 --- a/app/javascript/flavours/glitch/selectors/settings.ts +++ b/app/javascript/flavours/glitch/selectors/settings.ts @@ -17,10 +17,19 @@ export const selectSettingsNotificationsShows = createSelector( export const selectSettingsNotificationsExcludedTypes = createSelector( [selectSettingsNotificationsShows], - (shows) => - Object.entries(shows) - .filter(([_type, enabled]) => !enabled) - .map(([type, _enabled]) => type), + (shows) => { + const excludedTypes: string[] = []; + for (const key in shows) { + if (!shows[key]) { + if (key === 'collections') { + excludedTypes.push('collection_update', 'added_to_collection'); + } else { + excludedTypes.push(key); + } + } + } + return excludedTypes; + }, ); export const selectSettingsNotificationsQuickFilterShow = (state: RootState) =>