Handle unread notifications

Renaud Chaput 2024-06-18 11:03:47 +02:00
parent f368bfcde4
commit 021ce3663e
No known key found for this signature in database
GPG Key ID: BCFC859D49B46990
4 changed files with 62 additions and 48 deletions

View File

@ -19,6 +19,7 @@ import { Icon } from 'mastodon/components/icon';
import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator';
import { useIdentity } from 'mastodon/identity_context'; import { useIdentity } from 'mastodon/identity_context';
import type { NotificationGap } from 'mastodon/reducers/notifications_groups'; import type { NotificationGap } from 'mastodon/reducers/notifications_groups';
import { selectUnreadNotificationsGroupsCount } from 'mastodon/selectors/notifications';
import { import {
selectNeedsNotificationPermission, selectNeedsNotificationPermission,
selectSettingsNotificationsExcludedTypes, selectSettingsNotificationsExcludedTypes,
@ -35,8 +36,8 @@ import {
expandNotifications, expandNotifications,
scrollTopNotifications, scrollTopNotifications,
loadPending, loadPending,
mountNotifications, // mountNotifications,
unmountNotifications, // unmountNotifications,
markNotificationsAsRead, markNotificationsAsRead,
} from '../../actions/notifications'; } from '../../actions/notifications';
import Column from '../../components/column'; import Column from '../../components/column';
@ -81,52 +82,35 @@ const getNotifications = createSelector(
); );
// const mapStateToProps = (state) => ({ // const mapStateToProps = (state) => ({
// isUnread:
// state.getIn(['notifications', 'unread']) > 0 ||
// state.getIn(['notifications', 'pendingItems']).size > 0,
// numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()) // numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList())
// .size, // .size,
// 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,
// ),
// }); // });
export const Notifications: React.FC<{ export const Notifications: React.FC<{
columnId?: string; columnId?: string;
isUnread?: boolean;
multiColumn?: boolean; multiColumn?: boolean;
numPending: number; numPending: number;
}> = ({ isUnread, columnId, multiColumn, numPending }) => { }> = ({ columnId, multiColumn, numPending }) => {
const intl = useIntl(); const intl = useIntl();
const notifications = useAppSelector(getNotifications); const notifications = useAppSelector(getNotifications);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isLoading = useAppSelector((s) => s.notificationsGroups.isLoading); const isLoading = useAppSelector((s) => s.notificationsGroups.isLoading);
const hasMore = useAppSelector((s) => s.notificationsGroups.hasMore); const hasMore = useAppSelector((s) => s.notificationsGroups.hasMore);
const readMarkerId = useAppSelector(
(s) => s.notificationsGroups.readMarkerId,
);
const lastReadId = useAppSelector((s) => const lastReadId = useAppSelector((s) =>
selectSettingsNotificationsShowUnread(s) selectSettingsNotificationsShowUnread(s) ? s.markers.notifications : '0',
? s.notificationsGroups.readMarkerId
: '0',
); );
const canMarkAsRead = useAppSelector(
(s) => const unreadNotificationsCount = useAppSelector(
selectSettingsNotificationsShowUnread(s) && selectUnreadNotificationsGroupsCount,
s.notificationsGroups.readMarkerId !== '0' &&
notifications.some(
(item) =>
item.type !== 'gap' && compareId(item.group_key, readMarkerId) > 0,
),
); );
const isUnread = unreadNotificationsCount > 0;
const canMarkAsRead =
useAppSelector(selectSettingsNotificationsShowUnread) &&
unreadNotificationsCount > 0;
const needsNotificationPermission = useAppSelector( const needsNotificationPermission = useAppSelector(
selectNeedsNotificationPermission, selectNeedsNotificationPermission,
); );
@ -157,14 +141,14 @@ export const Notifications: React.FC<{
}, []); }, []);
useEffect(() => { useEffect(() => {
dispatch(mountNotifications()); // dispatch(mountNotifications());
// FIXME: remove once this becomes the main implementation // FIXME: remove once this becomes the main implementation
void dispatch(fetchNotifications()); void dispatch(fetchNotifications());
return () => { return () => {
dispatch(unmountNotifications()); // dispatch(unmountNotifications());
dispatch(scrollTopNotifications(false)); // dispatch(scrollTopNotifications(false));
}; };
}, [dispatch]); }, [dispatch]);
@ -282,7 +266,9 @@ export const Notifications: React.FC<{
onMoveUp={handleMoveUp} onMoveUp={handleMoveUp}
onMoveDown={handleMoveDown} onMoveDown={handleMoveDown}
unread={ unread={
lastReadId !== '0' && compareId(item.group_key, lastReadId) > 0 lastReadId !== '0' &&
!!item.page_max_id &&
compareId(item.page_max_id, lastReadId) > 0
} }
/> />
), ),

View File

@ -34,6 +34,7 @@ import { NavigationPortal } from 'mastodon/components/navigation_portal';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state'; import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
import { transientSingleColumn } from 'mastodon/is_mobile'; import { transientSingleColumn } from 'mastodon/is_mobile';
import { selectUnreadNotificationsGroupsCount } from 'mastodon/selectors/notifications';
import ColumnLink from './column_link'; import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner'; import DisabledAccountBanner from './disabled_account_banner';
@ -62,14 +63,27 @@ const NotificationsLink = () => {
const count = useSelector(state => state.getIn(['notifications', 'unread'])); const count = useSelector(state => state.getIn(['notifications', 'unread']));
const intl = useIntl(); const intl = useIntl();
const newCount = useSelector(selectUnreadNotificationsGroupsCount);
return ( return (
<ColumnLink <>
transparent <ColumnLink
to='/notifications' key='notifications'
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={count} className='column-link__icon' />} transparent
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={count} className='column-link__icon' />} to='/notifications'
text={intl.formatMessage(messages.notifications)} 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)}
/>
<ColumnLink
key='notifications-v2'
transparent
to='/notifications_v2'
icon={<IconWithBadge id='bell' icon={NotificationsIcon} count={newCount} className='column-link__icon' />}
activeIcon={<IconWithBadge id='bell' icon={NotificationsActiveIcon} count={newCount} className='column-link__icon' />}
text={"New Notifications"}
/>
</>
); );
}; };

View File

@ -31,18 +31,14 @@ export interface NotificationGap {
interface NotificationGroupsState { interface NotificationGroupsState {
groups: (NotificationGroup | NotificationGap)[]; groups: (NotificationGroup | NotificationGap)[];
unread: number;
isLoading: boolean; isLoading: boolean;
hasMore: boolean; hasMore: boolean;
readMarkerId: string;
} }
const initialState: NotificationGroupsState = { const initialState: NotificationGroupsState = {
groups: [], groups: [],
unread: 0,
isLoading: false, isLoading: false,
hasMore: false, hasMore: false,
readMarkerId: '0',
}; };
function removeNotificationsForAccounts( function removeNotificationsForAccounts(
@ -172,7 +168,6 @@ export const notificationsGroupsReducer =
}) })
.addCase(clearNotifications.pending, (state) => { .addCase(clearNotifications.pending, (state) => {
state.groups = []; state.groups = [];
state.unread = 0;
state.hasMore = false; state.hasMore = false;
}) })
.addCase(blockAccountSuccess, (state, action) => { .addCase(blockAccountSuccess, (state, action) => {

View File

@ -0,0 +1,19 @@
import { createSelector } from '@reduxjs/toolkit';
import { compareId } from 'mastodon/compare_id';
import type { RootState } from 'mastodon/store';
export const selectUnreadNotificationsGroupsCount = createSelector(
[
(s: RootState) => s.markers.notifications,
(s: RootState) => s.notificationsGroups.groups,
],
(notificationMarker, groups) => {
return groups.filter(
(group) =>
group.type !== 'gap' &&
group.page_max_id &&
compareId(group.page_max_id, notificationMarker) > 0,
).length;
},
);