From aac2f1e9bacbd85050843a83205e2faa139a705f Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Mon, 17 Jun 2024 23:56:23 +0200 Subject: [PATCH] Handle removals from notification groups --- .../mastodon/actions/notification_groups.ts | 12 ++- .../mastodon/actions/notifications.js | 13 +-- app/javascript/mastodon/api/notifications.ts | 5 +- .../mastodon/reducers/notifications.js | 4 +- .../mastodon/reducers/notifications_groups.ts | 94 +++++++++++++++++++ 5 files changed, 112 insertions(+), 16 deletions(-) diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 34e62054c6..464658851f 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -1,4 +1,7 @@ -import { apiFetchNotifications } from 'mastodon/api/notifications'; +import { + apiClearNotifications, + apiFetchNotifications, +} from 'mastodon/api/notifications'; import type { ApiAccountJSON } from 'mastodon/api_types/accounts'; import type { ApiNotificationGroupJSON, @@ -103,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk( export const processNewNotificationForGroups = createAppAsyncThunk( 'notificationsGroups/processNew', - (notification: NotificationJSON, { dispatch }) => { + (notification: ApiNotificationJSON, { dispatch }) => { dispatchAssociatedRecords(dispatch, [notification]); return notification; @@ -123,3 +126,8 @@ export const setNotificationsFilter = createAppAsyncThunk( dispatch(saveSettings()); }, ); + +export const clearNotifications = createDataLoadingThunk( + 'notifications/clear', + () => apiClearNotifications(), +); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 81217044cb..74c48ce3bb 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -24,6 +24,8 @@ import { saveSettings } from './settings'; export * from "./notifications_typed"; +export { clearNotifications } from "./notification_groups"; + export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; @@ -32,7 +34,6 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; @@ -257,16 +258,6 @@ export function expandNotificationsFail(error, isLoadingMore) { }; } -export function clearNotifications() { - return (dispatch) => { - dispatch({ - type: NOTIFICATIONS_CLEAR, - }); - - api().post('/api/v1/notifications/clear'); - }; -} - export function scrollTopNotifications(top) { return { type: NOTIFICATIONS_SCROLL_TOP, diff --git a/app/javascript/mastodon/api/notifications.ts b/app/javascript/mastodon/api/notifications.ts index abc83882b1..315c477ba4 100644 --- a/app/javascript/mastodon/api/notifications.ts +++ b/app/javascript/mastodon/api/notifications.ts @@ -1,4 +1,4 @@ -import api, { getLinks } from 'mastodon/api'; +import api, { apiRequest, getLinks } from 'mastodon/api'; import type { ApiNotificationGroupJSON } from 'mastodon/api_types/notifications'; export const apiFetchNotifications = async ( @@ -15,3 +15,6 @@ export const apiFetchNotifications = async ( return { notifications: response.data, links: getLinks(response) }; }; + +export const apiClearNotifications = () => + apiRequest('POST', 'v1/notifications/clear'); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 79aa5651ff..01ffd45b31 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -22,7 +22,6 @@ import { NOTIFICATIONS_EXPAND_REQUEST, NOTIFICATIONS_EXPAND_FAIL, NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, NOTIFICATIONS_LOAD_PENDING, NOTIFICATIONS_MOUNT, @@ -30,6 +29,7 @@ import { NOTIFICATIONS_MARK_AS_READ, NOTIFICATIONS_SET_BROWSER_SUPPORT, NOTIFICATIONS_SET_BROWSER_PERMISSION, + clearNotifications, } from '../actions/notifications'; import { disconnectTimeline } from '../actions/timelines'; import { compareId } from '../compare_id'; @@ -290,7 +290,7 @@ export default function notifications(state = initialState, action) { case authorizeFollowRequestSuccess.type: case rejectFollowRequestSuccess.type: return filterNotifications(state, [action.payload.id], 'follow_request'); - case NOTIFICATIONS_CLEAR: + case clearNotifications.pending.type: return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case timelineDelete.type: return deleteByStatus(state, action.payload.statusId); diff --git a/app/javascript/mastodon/reducers/notifications_groups.ts b/app/javascript/mastodon/reducers/notifications_groups.ts index de4bab5afe..a9b73ff046 100644 --- a/app/javascript/mastodon/reducers/notifications_groups.ts +++ b/app/javascript/mastodon/reducers/notifications_groups.ts @@ -1,10 +1,22 @@ import { createReducer, isAnyOf } from '@reduxjs/toolkit'; import { + authorizeFollowRequestSuccess, + blockAccountSuccess, + muteAccountSuccess, + rejectFollowRequestSuccess, +} from 'mastodon/actions/accounts_typed'; +import { blockDomainSuccess } from 'mastodon/actions/domain_blocks_typed'; +import { + clearNotifications, fetchNotifications, fetchNotificationsGap, processNewNotificationForGroups, } from 'mastodon/actions/notification_groups'; +import { + disconnectTimeline, + timelineDelete, +} from 'mastodon/actions/timelines_typed'; import { NOTIFICATIONS_GROUP_MAX_AVATARS, createNotificationGroupFromJSON, @@ -33,6 +45,48 @@ const initialState: NotificationGroupsState = { readMarkerId: '0', }; +function removeNotificationsForAccounts( + state: NotificationGroupsState, + accountIds: string[], + onlyForType?: string, +) { + state.groups = state.groups + .map((group) => { + if ( + group.type !== 'gap' && + (!onlyForType || group.type === onlyForType) + ) { + const previousLength = group.sampleAccountsIds.length; + + group.sampleAccountsIds = group.sampleAccountsIds.filter( + (id) => !accountIds.includes(id), + ); + + const newLength = group.sampleAccountsIds.length; + const removed = previousLength - newLength; + + group.notifications_count -= removed; + } + + return group; + }) + .filter( + (group) => group.type === 'gap' || group.sampleAccountsIds.length > 0, + ); +} + +function removeNotificationsForStatus( + state: NotificationGroupsState, + statusId: string, +) { + state.groups = state.groups.filter( + (group) => + group.type === 'gap' || + !('statusId' in group) || + group.statusId !== statusId, + ); +} + export const notificationsGroupsReducer = createReducer(initialState, (builder) => { builder @@ -106,6 +160,46 @@ export const notificationsGroupsReducer = ); } }) + .addCase(disconnectTimeline, (state, action) => { + if (action.payload.timeline === 'home') + state.groups.unshift({ + type: 'gap', + loadUrl: 'TODO_LOAD_URL_TOP_OF_TL', // TODO + }); + }) + .addCase(timelineDelete, (state, action) => { + removeNotificationsForStatus(state, action.payload.statusId); + }) + .addCase(clearNotifications.pending, (state) => { + state.groups = []; + state.unread = 0; + state.hasMore = false; + }) + .addCase(blockAccountSuccess, (state, action) => { + removeNotificationsForAccounts(state, [action.payload.relationship.id]); + }) + .addCase(muteAccountSuccess, (state, action) => { + if (action.payload.relationship.muting_notifications) + removeNotificationsForAccounts(state, [ + action.payload.relationship.id, + ]); + }) + .addCase(blockDomainSuccess, (state, action) => { + removeNotificationsForAccounts( + state, + action.payload.accounts.map((account) => account.id), + ); + }) + .addMatcher( + isAnyOf(authorizeFollowRequestSuccess, rejectFollowRequestSuccess), + (state, action) => { + removeNotificationsForAccounts( + state, + [action.payload.id], + 'follow_request', + ); + }, + ) .addMatcher( isAnyOf(fetchNotifications.pending, fetchNotificationsGap.pending), (state) => {