[WiP] Remove old notifications code

This commit is contained in:
Claire 2024-09-10 16:51:21 +02:00
parent cdcd834f3c
commit e79ed3dcab
14 changed files with 23 additions and 409 deletions

View File

@ -2,7 +2,6 @@ import { debounce } from 'lodash';
import type { MarkerJSON } from 'mastodon/api_types/markers';
import { getAccessToken } from 'mastodon/initial_state';
import { selectUseGroupedNotifications } from 'mastodon/selectors/settings';
import type { AppDispatch, RootState } from 'mastodon/store';
import { createAppAsyncThunk } from 'mastodon/store/typed_functions';
@ -76,12 +75,7 @@ interface MarkerParam {
}
function getLastNotificationId(state: RootState): string | undefined {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return selectUseGroupedNotifications(state)
? state.notificationGroups.lastReadId
: // @ts-expect-error state.notifications is not yet typed
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
state.getIn(['notifications', 'lastReadId']);
return state.notificationGroups.lastReadId;
}
const buildPostMarkersParams = (state: RootState) => {

View File

@ -1,14 +1,10 @@
import { selectUseGroupedNotifications } from 'mastodon/selectors/settings';
import { createAppAsyncThunk } from 'mastodon/store';
import { fetchNotifications } from './notification_groups';
import { expandNotifications } from './notifications';
export const initializeNotifications = createAppAsyncThunk(
'notifications/initialize',
(_, { dispatch, getState }) => {
if (selectUseGroupedNotifications(getState()))
void dispatch(fetchNotifications());
else void dispatch(expandNotifications({}));
(_, { dispatch }) => {
void dispatch(fetchNotifications());
},
);

View File

@ -1,7 +1,5 @@
// @ts-check
import { selectUseGroupedNotifications } from 'mastodon/selectors/settings';
import { getLocale } from '../locales';
import { connectStream } from '../stream';
@ -13,7 +11,7 @@ import {
} from './announcements';
import { updateConversations } from './conversations';
import { processNewNotificationForGroups, refreshStaleNotificationGroups, pollRecentNotifications as pollRecentGroupNotifications } from './notification_groups';
import { updateNotifications, expandNotifications } from './notifications';
import { updateNotifications } from './notifications';
import { updateStatus } from './statuses';
import {
updateTimeline,
@ -39,7 +37,7 @@ const randomUpTo = max =>
* @param {string} channelName
* @param {Object.<string, string>} params
* @param {Object} options
* @param {function(Function, Function): Promise<void>} [options.fallback]
* @param {function(Function): Promise<void>} [options.fallback]
* @param {function(): void} [options.fillGaps]
* @param {function(object): boolean} [options.accept]
* @returns {function(): void}
@ -54,11 +52,11 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
let pollingId;
/**
* @param {function(Function, Function): Promise<void>} fallback
* @param {function(Function): Promise<void>} fallback
*/
const useFallback = async fallback => {
await fallback(dispatch, getState);
await fallback(dispatch);
// eslint-disable-next-line react-hooks/rules-of-hooks -- this is not a react hook
pollingId = setTimeout(() => useFallback(fallback), 20000 + randomUpTo(20000));
};
@ -103,20 +101,13 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
case 'notification': {
// @ts-expect-error
const notificationJSON = JSON.parse(data.payload);
dispatch(updateNotifications(notificationJSON, messages, locale));
// TODO: remove this once the groups feature replaces the previous one
if(selectUseGroupedNotifications(getState())) {
dispatch(processNewNotificationForGroups(notificationJSON));
}
dispatch(updateNotifications(notificationJSON, messages, locale));
dispatch(processNewNotificationForGroups(notificationJSON));
break;
}
case 'notifications_merged':
const state = getState();
if (state.notifications.top || !state.notifications.mounted)
dispatch(expandNotifications({ forceLoad: true, maxId: undefined }));
if (selectUseGroupedNotifications(state)) {
dispatch(refreshStaleNotificationGroups());
}
dispatch(refreshStaleNotificationGroups());
break;
case 'conversation':
// @ts-expect-error
@ -141,21 +132,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
/**
* @param {Function} dispatch
* @param {Function} getState
*/
async function refreshHomeTimelineAndNotification(dispatch, getState) {
async function refreshHomeTimelineAndNotification(dispatch) {
await dispatch(expandHomeTimeline({ maxId: undefined }));
// TODO: remove this once the groups feature replaces the previous one
if(selectUseGroupedNotifications(getState())) {
// TODO: polling for merged notifications
try {
await dispatch(pollRecentGroupNotifications());
} catch (error) {
// TODO
}
} else {
await dispatch(expandNotifications({}));
try {
await dispatch(pollRecentGroupNotifications());
} catch (error) {
// TODO
}
await dispatch(fetchAnnouncements());

View File

@ -6,7 +6,6 @@ import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { forceGroupedNotifications } from 'mastodon/initial_state';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_REPORTS } from 'mastodon/permissions';
import ClearColumnButton from './clear_column_button';
@ -36,7 +35,6 @@ class ColumnSettings extends PureComponent {
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' />;
const groupingShowStr = <FormattedMessage id='notifications.column_settings.beta.grouping' defaultMessage='Group notifications' />;
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' />;
@ -68,18 +66,6 @@ class ColumnSettings extends PureComponent {
<PolicyControls />
{!forceGroupedNotifications && (
<section role='group' aria-labelledby='notifications-beta'>
<h3 id='notifications-beta'>
<FormattedMessage id='notifications.column_settings.beta.category' defaultMessage='Experimental features' />
</h3>
<div className='column-settings__row'>
<SettingToggle id='unread-notification-markers' prefix='notifications' settings={settings} settingPath={['groupingBeta']} onChange={onChange} label={groupingShowStr} />
</div>
</section>
)}
<section role='group' aria-labelledby='notifications-unread-markers'>
<h3 id='notifications-unread-markers'>
<FormattedMessage id='notifications.column_settings.unread_notifications.category' defaultMessage='Unread notifications' />

View File

@ -3,7 +3,6 @@ import { defineMessages, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
import { initializeNotifications } from 'mastodon/actions/notifications_migration';
import { showAlert } from '../../../actions/alerts';
import { setFilter, requestBrowserPermission } from '../../../actions/notifications';
@ -56,9 +55,6 @@ const mapDispatchToProps = (dispatch) => ({
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}
} else if(path[0] === 'groupingBeta') {
dispatch(changeSetting(['notifications', ...path], checked));
dispatch(initializeNotifications());
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}

View File

@ -1,308 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { Helmet } from 'react-helmet';
import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { debounce } from 'lodash';
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
import { compareId } from 'mastodon/compare_id';
import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { submitMarkers } from '../../actions/markers';
import {
expandNotifications,
scrollTopNotifications,
loadPending,
mountNotifications,
unmountNotifications,
markNotificationsAsRead,
} from '../../actions/notifications';
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,
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';
import NotificationContainer from './containers/notification_container';
const messages = defineMessages({
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
});
const getExcludedTypes = createSelector([
state => state.getIn(['settings', 'notifications', 'shows']),
], (shows) => {
return ImmutableList(shows.filter(item => !item).keys());
});
const getNotifications = createSelector([
state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
getExcludedTypes,
state => state.getIn(['notifications', 'items']),
], (showFilterBar, allowedType, excludedTypes, notifications) => {
if (!showFilterBar || allowedType === 'all') {
// used if user changed the notification settings after loading the notifications from the server
// otherwise a list of notifications will come pre-filtered from the backend
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
}
return notifications.filter(item => item === null || allowedType === item.get('type'));
});
const mapStateToProps = state => ({
notifications: getNotifications(state),
isLoading: state.getIn(['notifications', 'isLoading'], 0) > 0,
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
hasMore: state.getIn(['notifications', 'hasMore']),
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
lastReadId: state.getIn(['settings', 'notifications', 'showUnread']) ? state.getIn(['notifications', 'readMarkerId']) : '0',
canMarkAsRead: state.getIn(['settings', 'notifications', 'showUnread']) && state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default' && !state.getIn(['settings', 'notifications', 'dismissPermissionBanner']),
});
class Notifications extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
columnId: PropTypes.string,
notifications: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
hasMore: PropTypes.bool,
numPending: PropTypes.number,
lastReadId: PropTypes.string,
canMarkAsRead: PropTypes.bool,
needsNotificationPermission: PropTypes.bool,
};
static defaultProps = {
trackScroll: true,
};
UNSAFE_componentWillMount() {
this.props.dispatch(mountNotifications());
}
componentWillUnmount () {
this.handleLoadOlder.cancel();
this.handleScrollToTop.cancel();
this.handleScroll.cancel();
this.props.dispatch(scrollTopNotifications(false));
this.props.dispatch(unmountNotifications());
}
handleLoadGap = (maxId) => {
this.props.dispatch(expandNotifications({ maxId }));
};
handleLoadOlder = debounce(() => {
const last = this.props.notifications.last();
this.props.dispatch(expandNotifications({ maxId: last && last.get('id') }));
}, 300, { leading: true });
handleLoadPending = () => {
this.props.dispatch(loadPending());
};
handleScrollToTop = debounce(() => {
this.props.dispatch(scrollTopNotifications(true));
}, 100);
handleScroll = debounce(() => {
this.props.dispatch(scrollTopNotifications(false));
}, 100);
handlePin = () => {
const { columnId, dispatch } = this.props;
if (columnId) {
dispatch(removeColumn(columnId));
} else {
dispatch(addColumn('NOTIFICATIONS', {}));
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
};
handleHeaderClick = () => {
this.column.scrollTop();
};
setColumnRef = c => {
this.column = c;
};
handleMoveUp = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
this._selectChild(elementIndex, true);
};
handleMoveDown = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
this._selectChild(elementIndex, false);
};
_selectChild (index, align_top) {
const container = this.column.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
if (element) {
if (align_top && container.scrollTop > element.offsetTop) {
element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false);
}
element.focus();
}
}
handleMarkAsRead = () => {
this.props.dispatch(markNotificationsAsRead());
this.props.dispatch(submitMarkers({ immediate: true }));
};
render () {
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
const { signedIn } = this.props.identity;
let scrollableContent = null;
const filterBarContainer = signedIn
? (<FilterBarContainer />)
: null;
if (isLoading && this.scrollableContent) {
scrollableContent = this.scrollableContent;
} else if (notifications.size > 0 || hasMore) {
scrollableContent = notifications.map((item, index) => item === null ? (
<LoadGap
key={'gap:' + notifications.getIn([index + 1, 'id'])}
disabled={isLoading}
param={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
onClick={this.handleLoadGap}
/>
) : (
<NotificationContainer
key={item.get('id')}
notification={item}
accountId={item.get('account')}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
unread={lastReadId !== '0' && compareId(item.get('id'), lastReadId) > 0}
/>
));
} else {
scrollableContent = null;
}
this.scrollableContent = scrollableContent;
let scrollContainer;
const prepend = (
<>
{needsNotificationPermission && <NotificationsPermissionBanner />}
<FilteredNotificationsBanner />
</>
);
if (signedIn) {
scrollContainer = (
<ScrollableList
scrollKey={`notifications-${columnId}`}
trackScroll={!pinned}
isLoading={isLoading}
showLoading={isLoading && notifications.size === 0}
hasMore={hasMore}
numPending={numPending}
prepend={prepend}
alwaysPrepend
emptyMessage={emptyMessage}
onLoadMore={this.handleLoadOlder}
onLoadPending={this.handleLoadPending}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
bindToDocument={!multiColumn}
>
{scrollableContent}
</ScrollableList>
);
} else {
scrollContainer = <NotSignedInIndicator />;
}
const extraButton = (
<>
<FilteredNotificationsIconButton className='column-header__button' />
{canMarkAsRead && (
<button
aria-label={intl.formatMessage(messages.markAsRead)}
title={intl.formatMessage(messages.markAsRead)}
onClick={this.handleMarkAsRead}
className='column-header__button'
>
<Icon id='done-all' icon={DoneAllIcon} />
</button>
)}
</>
);
return (
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader
icon='bell'
iconComponent={NotificationsIcon}
active={isUnread}
title={intl.formatMessage(messages.title)}
onPin={this.handlePin}
onMove={this.handleMove}
onClick={this.handleHeaderClick}
pinned={pinned}
multiColumn={multiColumn}
extraButton={extraButton}
>
<ColumnSettingsContainer />
</ColumnHeader>
{filterBarContainer}
{scrollContainer}
<Helmet>
<title>{intl.formatMessage(messages.title)}</title>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}
export default connect(mapStateToProps)(withIdentity(injectIntl(Notifications)));

View File

@ -1,14 +0,0 @@
import Notifications from 'mastodon/features/notifications';
import Notifications_v2 from 'mastodon/features/notifications_v2';
import { selectUseGroupedNotifications } from 'mastodon/selectors/settings';
import { useAppSelector } from 'mastodon/store';
export const NotificationsWrapper = (props) => {
const optedInGroupedNotifications = useAppSelector(selectUseGroupedNotifications);
return (
optedInGroupedNotifications ? <Notifications_v2 {...props} /> : <Notifications {...props} />
);
};
export default NotificationsWrapper;

View File

@ -10,7 +10,7 @@ import { scrollRight } from '../../../scroll';
import BundleContainer from '../containers/bundle_container';
import {
Compose,
NotificationsWrapper,
Notifications,
HomeTimeline,
CommunityTimeline,
PublicTimeline,
@ -32,7 +32,7 @@ import NavigationPanel from './navigation_panel';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
'NOTIFICATIONS': NotificationsWrapper,
'NOTIFICATIONS': Notifications,
'PUBLIC': PublicTimeline,
'REMOTE': PublicTimeline,
'COMMUNITY': CommunityTimeline,

View File

@ -37,7 +37,6 @@ import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile';
import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
import { selectUseGroupedNotifications } from 'mastodon/selectors/settings';
import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner';
@ -65,19 +64,16 @@ const messages = defineMessages({
});
const NotificationsLink = () => {
const optedInGroupedNotifications = useSelector(selectUseGroupedNotifications);
const count = useSelector(state => state.getIn(['notifications', 'unread']));
const count = useSelector(selectUnreadNotificationGroupsCount);
const intl = useIntl();
const newCount = useSelector(selectUnreadNotificationGroupsCount);
return (
<ColumnLink
key='notifications'
transparent
to='/notifications'
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={optedInGroupedNotifications ? newCount : count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={optedInGroupedNotifications ? newCount : count} className='column-link__icon' />}
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={count} className='column-link__icon' />}
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={count} className='column-link__icon' />}
text={intl.formatMessage(messages.notifications)}
/>
);

View File

@ -49,7 +49,7 @@ import {
Favourites,
DirectTimeline,
HashtagTimeline,
NotificationsWrapper,
Notifications,
NotificationRequests,
NotificationRequest,
FollowRequests,
@ -206,7 +206,7 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path='/tags/:id' component={HashtagTimeline} content={children} />
<WrappedRoute path='/links/:url' component={LinkTimeline} content={children} />
<WrappedRoute path='/lists/:id' component={ListTimeline} content={children} />
<WrappedRoute path='/notifications' component={NotificationsWrapper} content={children} exact />
<WrappedRoute path='/notifications' component={Notifications} content={children} exact />
<WrappedRoute path='/notifications/requests' component={NotificationRequests} content={children} exact />
<WrappedRoute path='/notifications/requests/:id' component={NotificationRequest} content={children} exact />
<WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />

View File

@ -7,15 +7,7 @@ export function Compose () {
}
export function Notifications () {
return import(/* webpackChunkName: "features/notifications_v1" */'../../notifications');
}
export function Notifications_v2 () {
return import(/* webpackChunkName: "features/notifications_v2" */'../../notifications_v2');
}
export function NotificationsWrapper () {
return import(/* webpackChunkName: "features/notifications" */'../../notifications_wrapper');
return import(/* webpackChunkName: "features/notifications" */'../../notifications_v2');
}
export function HomeTimeline () {

View File

@ -43,7 +43,6 @@
* @property {boolean=} use_pending_items
* @property {string} version
* @property {string} sso_redirect
* @property {boolean} force_grouped_notifications
*/
/**
@ -119,7 +118,6 @@ export const criticalUpdatesPending = initialState?.critical_updates_pending;
// @ts-expect-error
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
export const forceGroupedNotifications = getMeta('force_grouped_notifications');
/**
* @returns {string | undefined}

View File

@ -1,4 +1,3 @@
import { forceGroupedNotifications } from 'mastodon/initial_state';
import type { RootState } from 'mastodon/store';
/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
@ -26,10 +25,6 @@ export const selectSettingsNotificationsQuickFilterAdvanced = (
) =>
state.settings.getIn(['notifications', 'quickFilter', 'advanced']) as boolean;
export const selectUseGroupedNotifications = (state: RootState) =>
forceGroupedNotifications ||
(state.settings.getIn(['notifications', 'groupingBeta']) as boolean);
export const selectSettingsNotificationsShowUnread = (state: RootState) =>
state.settings.getIn(['notifications', 'showUnread']) as boolean;

View File

@ -109,7 +109,6 @@ class InitialStateSerializer < ActiveModel::Serializer
trends_as_landing_page: Setting.trends_as_landing_page,
trends_enabled: Setting.trends,
version: instance_presenter.version,
force_grouped_notifications: ENV['FORCE_GROUPED_NOTIFICATIONS'] == 'true',
}
end