From fe5653cce950debfc80ec5f8bb95de0d40c1a955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 3 Nov 2024 00:18:29 +0100 Subject: [PATCH] Mostly migrate pl-fe notifications to notification groups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-api/lib/client.ts | 100 ++++++++++- .../entities/grouped-notifications-results.ts | 11 +- packages/pl-api/lib/entities/index.ts | 1 + .../lib/params/grouped-notifications.ts | 15 +- packages/pl-api/lib/params/index.ts | 1 + packages/pl-fe/src/actions/notifications.ts | 94 +++++----- .../notifications/components/notification.tsx | 22 +-- .../src/features/notifications/index.tsx | 17 +- .../pl-fe/src/normalizers/notification.ts | 73 ++------ packages/pl-fe/src/reducers/notifications.ts | 161 +++++++++--------- packages/pl-fe/src/selectors/index.ts | 29 ++-- 11 files changed, 301 insertions(+), 223 deletions(-) diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index bcfe98eea..89774f1d2 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -1,4 +1,5 @@ import omit from 'lodash.omit'; +import pick from 'lodash.pick'; import * as v from 'valibot'; import { @@ -110,7 +111,7 @@ import { MuteAccountParams, UpdateFilterParams, } from './params/filtering'; -import { GetGroupedNotificationsParams } from './params/grouped-notifications'; +import { GetGroupedNotificationsParams, GetUnreadNotificationGroupCountParams } from './params/grouped-notifications'; import { CreateGroupParams, GetGroupBlocksParams, @@ -193,6 +194,7 @@ import request, { getNextLink, getPrevLink, type RequestBody, RequestMeta } from import { buildFullPath } from './utils/url'; import type { + Account, AdminAccount, AdminAnnouncement, AdminModerationLogEntry, @@ -299,6 +301,28 @@ class PlApiClient { }; }; + #paginatedSingleGet = async (input: URL | RequestInfo, body: RequestBody, schema: v.BaseSchema>): Promise> => { + const getMore = (input: string | null) => input ? async () => { + const response = await this.request(input); + + return { + previous: getMore(getPrevLink(response)), + next: getMore(getNextLink(response)), + items: v.parse(schema, response.json), + partial: response.status === 206, + }; + } : null; + + const response = await this.request(input, body); + + return { + previous: getMore(getPrevLink(response)), + next: getMore(getNextLink(response)), + items: v.parse(schema, response.json), + partial: response.status === 206, + }; + }; + #paginatedPleromaAccounts = async (params: { query?: string; filters?: string; @@ -398,11 +422,16 @@ class PlApiClient { } const groupedNotificationsResults: GroupedNotificationsResults = { - accounts: items.map(({ account }) => account), - statuses: items.reduce>((statuses, notification) => { - if ('status' in notification) statuses.push(notification.status); + accounts: Object.values(items.reduce>((accounts, notification) => { + accounts[notification.account.id] = notification.account; + if ('target' in notification) accounts[notification.target.id] = notification.target; + + return accounts; + }, {})), + statuses: Object.values(items.reduce>((statuses, notification) => { + if ('status' in notification) statuses[notification.status.id] = notification.status; return statuses; - }, []), + }, {})), notification_groups: notificationGroups, }; @@ -2767,11 +2796,66 @@ class PlApiClient { }; public readonly groupedNotifications = { - getGroupedNotifications: async (params: GetGroupedNotificationsParams) => { + getGroupedNotifications: async (params: GetGroupedNotificationsParams, meta?: RequestMeta) => { if (this.features.groupedNotifications) { - return this.#paginatedGet('/api/v2/notifications', { params }, groupedNotificationsResultsSchema); + return this.#paginatedSingleGet('/api/v2/notifications', { ...meta, params }, groupedNotificationsResultsSchema); } else { - return this.#groupNotifications(await this.notifications.getNotifications(), params); + const response = await this.notifications.getNotifications( + pick(params, ['max_id', 'since_id', 'limit', 'min_id', 'types', 'exclude_types', 'account_id', 'include_filtered']), + ); + + return this.#groupNotifications(response, params); + } + }, + + getNotificationGroup: async (groupKey: string) => { + if (this.features.groupedNotifications) { + const response = await this.request(`/api/v2/notifications/${groupKey}`); + + return v.parse(groupedNotificationsResultsSchema, response.json); + } else { + const response = await this.request(`/api/v1/notifications/${groupKey}`); + + return this.#groupNotifications({ + previous: null, + next: null, + items: [response.json], + partial: false, + }).items; + } + }, + + dismissNotificationGroup: async (groupKey: string) => { + if (this.features.groupedNotifications) { + const response = await this.request(`/api/v2/notifications/${groupKey}/dismiss`, { method: 'POST' }); + + return response.json as {}; + } else { + return this.notifications.dismissNotification(groupKey); + } + }, + + getNotificationGroupAccounts: async (groupKey: string) => { + if (this.features.groupedNotifications) { + const response = await this.request(`/api/v2/notifications/${groupKey}/accounts`); + + return v.parse(filteredArray(accountSchema), response.json); + } else { + return (await (this.groupedNotifications.getNotificationGroup(groupKey))).accounts; + } + }, + + getUnreadNotificationGroupCount: async (params: GetUnreadNotificationGroupCountParams) => { + if (this.features.groupedNotifications) { + const response = await this.request('/api/v2/notifications/unread_count', { params }); + + return v.parse(v.object({ + count: v.number(), + }), response.json); + } else { + return this.notifications.getUnreadNotificationCount( + pick(params || {}, ['max_id', 'since_id', 'limit', 'min_id', 'types', 'exclude_types', 'account_id']), + ); } }, }; diff --git a/packages/pl-api/lib/entities/grouped-notifications-results.ts b/packages/pl-api/lib/entities/grouped-notifications-results.ts index 646276c47..53eeadece 100644 --- a/packages/pl-api/lib/entities/grouped-notifications-results.ts +++ b/packages/pl-api/lib/entities/grouped-notifications-results.ts @@ -38,9 +38,16 @@ const accountNotificationGroupSchema = v.object({ type: v.picklist(['follow', 'follow_request', 'admin.sign_up', 'bite']), }); +const mentionNotificationGroupSchema = v.object({ + ...baseNotificationGroupSchema.entries, + type: v.literal('mention'), + subtype: v.fallback(v.nullable(v.picklist(['reply'])), null), + status_id: v.string(), +}); + const statusNotificationGroupSchema = v.object({ ...baseNotificationGroupSchema.entries, - type: v.picklist(['status', 'mention', 'reblog', 'favourite', 'poll', 'update', 'event_reminder']), + type: v.picklist(['status', 'reblog', 'favourite', 'poll', 'update', 'event_reminder']), status_id: v.string(), }); @@ -105,6 +112,7 @@ const notificationGroupSchema: v.BaseSchema; + /** Types of notifications that should not count towards unread notifications. */ + exclude_types?: Array; + /** Only count unread notifications received from the specified account. */ + account_id?: string; + /** Restrict which notification types can be grouped. Use this if there are notification types for which your client does not support grouping. If omitted, the server will group notifications of all types it supports (currently, `favourite`, `follow` and `reblog`). If you do not want any notification grouping, use GET /api/v1/notifications/unread_count instead. */ + grouped_types?: Array; +} + +export type { GetGroupedNotificationsParams, GetUnreadNotificationGroupCountParams }; diff --git a/packages/pl-api/lib/params/index.ts b/packages/pl-api/lib/params/index.ts index dcf70254f..08b7056b0 100644 --- a/packages/pl-api/lib/params/index.ts +++ b/packages/pl-api/lib/params/index.ts @@ -4,6 +4,7 @@ export * from './apps'; export * from './chats'; export * from './events'; export * from './filtering'; +export * from './grouped-notifications'; export * from './groups'; export * from './instance'; export * from './interaction-requests'; diff --git a/packages/pl-fe/src/actions/notifications.ts b/packages/pl-fe/src/actions/notifications.ts index e41c7a091..a1ca22390 100644 --- a/packages/pl-fe/src/actions/notifications.ts +++ b/packages/pl-fe/src/actions/notifications.ts @@ -1,10 +1,11 @@ import IntlMessageFormat from 'intl-messageformat'; + import 'intl-pluralrules'; import { defineMessages } from 'react-intl'; import { getClient } from 'pl-fe/api'; import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification'; -import { normalizeNotification, normalizeNotifications, type Notification } from 'pl-fe/normalizers/notification'; +import { normalizeNotification } from 'pl-fe/normalizers/notification'; import { getFilters, regexFromFilters } from 'pl-fe/selectors'; import { useSettingsStore } from 'pl-fe/stores/settings'; import { isLoggedIn } from 'pl-fe/utils/auth'; @@ -18,7 +19,7 @@ import { importEntities } from './importer'; import { saveMarker } from './markers'; import { saveSettings } from './settings'; -import type { Account, Notification as BaseNotification, PaginatedResponse, Status } from 'pl-api'; +import type { Notification as BaseNotification, GetGroupedNotificationsParams, GroupedNotificationsResults, NotificationGroup, PaginatedSingleResponse } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const; @@ -58,8 +59,8 @@ defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, }); -const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array) => { - const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); +const fetchRelatedRelationships = (dispatch: AppDispatch, notifications: Array) => { + const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.sample_account_ids).flat(); if (accountIds.length > 0) { dispatch(fetchRelationships(accountIds)); @@ -76,13 +77,16 @@ const updateNotifications = (notification: BaseNotification) => statuses: [getNotificationStatus(notification)], })); + if (showInColumn) { + const normalizedNotification = normalizeNotification(notification); + dispatch({ type: NOTIFICATIONS_UPDATE, - notification: normalizeNotification(notification), + notification: normalizedNotification, }); - fetchRelatedRelationships(dispatch, [notification]); + fetchRelatedRelationships(dispatch, [normalizedNotification]); } }; @@ -195,7 +199,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an } } - const params: Record = { + const params: GetGroupedNotificationsParams = { max_id: maxId, }; @@ -203,7 +207,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an if (features.notificationsIncludeTypes) { params.types = NOTIFICATION_TYPES.filter(type => !EXCLUDE_TYPES.includes(type as any)); } else { - params.exclude_types = EXCLUDE_TYPES; + params.exclude_types = [...EXCLUDE_TYPES]; } } else { const filtered = FILTER_TYPES[activeFilter] || [activeFilter]; @@ -215,51 +219,65 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an } if (!maxId && notifications.items.size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + params.since_id = notifications.items.first()?.page_max_id; } dispatch(expandNotificationsRequest()); - return getClient(state).notifications.getNotifications(params, { signal: abortExpandNotifications.signal }).then(response => { - const entries = (response.items).reduce((acc, item) => { - if (item.account?.id) { - acc.accounts[item.account.id] = item.account; - } - - // Used by Move notification - if (item.type === 'move' && item.target.id) { - acc.accounts[item.target.id] = item.target; - } - - // TODO actually check for type - // @ts-ignore - if (item.status?.id) { - // @ts-ignore - acc.statuses[item.status.id] = item.status; - } - - return acc; - }, { accounts: {}, statuses: {} } as { accounts: Record; statuses: Record }); - + return getClient(state).groupedNotifications.getGroupedNotifications(params, { signal: abortExpandNotifications.signal }).then(({ items: { accounts, statuses, notification_groups }, next }) => { dispatch(importEntities({ - accounts: Object.values(entries.accounts), - statuses: Object.values(entries.statuses), + accounts, + statuses, })); - const deduplicatedNotifications = normalizeNotifications(response.items, state.notifications.items); - - dispatch(expandNotificationsSuccess(deduplicatedNotifications, response.next)); - fetchRelatedRelationships(dispatch, response.items); + dispatch(expandNotificationsSuccess(notification_groups, next)); + fetchRelatedRelationships(dispatch, notification_groups); done(); }).catch(error => { dispatch(expandNotificationsFail(error)); done(); }); + + // return getClient(state).notifications.getNotifications(params, { signal: abortExpandNotifications.signal }).then(response => { + // const entries = (response.items).reduce((acc, item) => { + // if (item.account?.id) { + // acc.accounts[item.account.id] = item.account; + // } + + // // Used by Move notification + // if (item.type === 'move' && item.target.id) { + // acc.accounts[item.target.id] = item.target; + // } + + // // TODO actually check for type + // // @ts-ignore + // if (item.status?.id) { + // // @ts-ignore + // acc.statuses[item.status.id] = item.status; + // } + + // return acc; + // }, { accounts: {}, statuses: {} } as { accounts: Record; statuses: Record }); + + // dispatch(importEntities({ + // accounts: Object.values(entries.accounts), + // statuses: Object.values(entries.statuses), + // })); + + // const deduplicatedNotifications = normalizeNotifications(response.items, state.notifications.items); + + // dispatch(expandNotificationsSuccess(deduplicatedNotifications, response.next)); + // fetchRelatedRelationships(dispatch, response.items); + // done(); + // }).catch(error => { + // dispatch(expandNotificationsFail(error)); + // done(); + // }); }; const expandNotificationsRequest = () => ({ type: NOTIFICATIONS_EXPAND_REQUEST }); -const expandNotificationsSuccess = (notifications: Array, next: (() => Promise>) | null) => ({ +const expandNotificationsSuccess = (notifications: Array, next: (() => Promise>) | null) => ({ type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, next, @@ -297,7 +315,7 @@ const markReadNotifications = () => if (!isLoggedIn(getState)) return; const state = getState(); - const topNotificationId = state.notifications.items.first()?.id; + const topNotificationId = state.notifications.items.first()?.page_max_id; const lastReadId = state.notifications.lastRead; if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) { diff --git a/packages/pl-fe/src/features/notifications/components/notification.tsx b/packages/pl-fe/src/features/notifications/components/notification.tsx index 582c77ed1..102045908 100644 --- a/packages/pl-fe/src/features/notifications/components/notification.tsx +++ b/packages/pl-fe/src/features/notifications/components/notification.tsx @@ -24,11 +24,9 @@ import { useModalsStore } from 'pl-fe/stores/modals'; import { useSettingsStore } from 'pl-fe/stores/settings'; import { NotificationType } from 'pl-fe/utils/notification'; -import type { Notification as BaseNotification } from 'pl-api'; +import type { NotificationGroup } from 'pl-api'; import type { Account } from 'pl-fe/normalizers/account'; -import type { Notification as NotificationEntity } from 'pl-fe/normalizers/notification'; import type { Status as StatusEntity } from 'pl-fe/normalizers/status'; -import type { MinifiedNotification } from 'pl-fe/reducers/notifications'; const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: string) => { const output = [message]; @@ -184,13 +182,13 @@ const avatarSize = 48; interface INotification { hidden?: boolean; - notification: MinifiedNotification; + notification: NotificationGroup; onMoveUp?: (notificationId: string) => void; onMoveDown?: (notificationId: string) => void; onReblog?: (status: StatusEntity, e?: KeyboardEvent) => void; } -const getNotificationStatus = (n: NotificationEntity | BaseNotification) => { +const getNotificationStatus = (n: Pick>, null>, 'type' | 'status'>) => { if (['mention', 'status', 'reblog', 'favourite', 'poll', 'update', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'].includes(n.type)) // @ts-ignore return n.status; @@ -207,15 +205,17 @@ const Notification: React.FC = (props) => { const { me } = useLoggedIn(); const { openModal } = useModalsStore(); const { settings } = useSettingsStore(); + const notification = useAppSelector((state) => getNotification(state, props.notification)); + const status = getNotificationStatus(notification); const history = useHistory(); const intl = useIntl(); const instance = useInstance(); const type = notification.type; - const { account, accounts } = notification; - const status = getNotificationStatus(notification); + const { accounts } = notification; + const account = accounts[0]; const getHandlers = () => ({ reply: handleMention, @@ -289,13 +289,13 @@ const Notification: React.FC = (props) => { const handleMoveUp = () => { if (onMoveUp) { - onMoveUp(notification.id); + onMoveUp(notification.group_key); } }; const handleMoveDown = () => { if (onMoveDown) { - onMoveDown(notification.id); + onMoveDown(notification.group_key); } }; @@ -393,7 +393,7 @@ const Notification: React.FC = (props) => { name: account && typeof account === 'object' ? account.acct : '', targetName, }), - notification.created_at, + notification.latest_page_notification_at!, ) ); @@ -433,7 +433,7 @@ const Notification: React.FC = (props) => { truncate data-testid='message' > - + )} diff --git a/packages/pl-fe/src/features/notifications/index.tsx b/packages/pl-fe/src/features/notifications/index.tsx index fedf50ff2..2af3eb865 100644 --- a/packages/pl-fe/src/features/notifications/index.tsx +++ b/packages/pl-fe/src/features/notifications/index.tsx @@ -32,7 +32,7 @@ const messages = defineMessages({ const getNotifications = createSelector([ (state: RootState) => state.notifications.items.toList(), -], (notifications) => notifications.filter(item => item !== null && !item.duplicate)); +], (notifications) => notifications.filter(item => item !== null)); const Notifications = () => { const dispatch = useAppDispatch(); @@ -54,8 +54,13 @@ const Notifications = () => { // }; const handleLoadOlder = useCallback(debounce(() => { - const last = notifications.last(); - dispatch(expandNotifications({ maxId: last && last.id })); + const minId = notifications.reduce( + (minId, notification) => minId && notification.page_min_id && notification.page_min_id > minId + ? minId + : notification.page_min_id, + undefined, + ); + dispatch(expandNotifications({ maxId: minId })); }, 300, { leading: true }), [notifications]); const handleScroll = useCallback(debounce((startIndex?: number) => { @@ -63,12 +68,12 @@ const Notifications = () => { }, 100), []); const handleMoveUp = (id: string) => { - const elementIndex = notifications.findIndex(item => item !== null && item.id === id) - 1; + const elementIndex = notifications.findIndex(item => item !== null && item.group_key === id) - 1; _selectChild(elementIndex); }; const handleMoveDown = (id: string) => { - const elementIndex = notifications.findIndex(item => item !== null && item.id === id) + 1; + const elementIndex = notifications.findIndex(item => item !== null && item.group_key === id) + 1; _selectChild(elementIndex); }; @@ -111,7 +116,7 @@ const Notifications = () => { } else if (notifications.size > 0 || hasMore) { scrollableContent = notifications.map((item) => ( ({ - ...notification, - duplicate: false, - account: normalizeAccount(notification.account), - account_id: notification.account.id, - accounts: [normalizeAccount(notification.account)], - account_ids: [notification.account.id], +const normalizeNotification = (notification: BaseNotification): NotificationGroup => ({ + ...(omit(notification, ['account', 'status', 'target'])), + group_key: notification.id, + notifications_count: 1, + most_recent_notification_id: notification.id, + page_min_id: notification.id, + page_max_id: notification.id, + latest_page_notification_at: notification.created_at, + sample_account_ids: [notification.account.id], + // @ts-ignore + status_id: notification.status?.id, + // @ts-ignore + target_id: notification.target?.id, }); -const normalizeNotifications = (notifications: Array, stateNotifications?: ImmutableOrderedMap) => { - const deduplicatedNotifications: Notification[] = []; - - for (const notification of notifications) { - const existingNotification = stateNotifications?.get(notification.id); - - // Do not update grouped notifications - if (existingNotification && (existingNotification.duplicate || existingNotification.account_ids.length)) continue; - - if (STATUS_NOTIFICATION_TYPES.includes(notification.type)) { - const existingNotification = deduplicatedNotifications - .find(deduplicated => - deduplicated.type === notification.type - && ((notification.type === 'emoji_reaction' && deduplicated.type === 'emoji_reaction') ? notification.emoji === deduplicated.emoji : true) - && getNotificationStatus(deduplicated)?.id === getNotificationStatus(notification)?.id, - ); - - if (existingNotification) { - existingNotification.accounts.push(normalizeAccount(notification.account)); - existingNotification.account_ids.push(notification.account.id); - deduplicatedNotifications.push({ ...normalizeNotification(notification), duplicate: true }); - } else { - deduplicatedNotifications.push(normalizeNotification(notification)); - } - } else { - deduplicatedNotifications.push(normalizeNotification(notification)); - } - } - - return deduplicatedNotifications; -}; - -type Notification = ReturnType; - -export { normalizeNotification, normalizeNotifications, type Notification }; +export { normalizeNotification }; diff --git a/packages/pl-fe/src/reducers/notifications.ts b/packages/pl-fe/src/reducers/notifications.ts index 445e406b1..3c4b63792 100644 --- a/packages/pl-fe/src/reducers/notifications.ts +++ b/packages/pl-fe/src/reducers/notifications.ts @@ -1,5 +1,4 @@ import { Record as ImmutableRecord, OrderedMap as ImmutableOrderedMap } from 'immutable'; -import omit from 'lodash/omit'; import { ACCOUNT_BLOCK_SUCCESS, @@ -27,18 +26,17 @@ import { } from '../actions/notifications'; import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines'; -import type { AccountWarning, Notification as BaseNotification, Markers, PaginatedResponse, Relationship, RelationshipSeveranceEvent, Report } from 'pl-api'; -import type { Notification } from 'pl-fe/normalizers/notification'; +import type { Notification as BaseNotification, Markers, NotificationGroup, PaginatedResponse, Relationship } from 'pl-api'; import type { AnyAction } from 'redux'; const QueuedNotificationRecord = ImmutableRecord({ - notification: {} as any as BaseNotification, + notification: {} as any as NotificationGroup, intlMessages: {} as Record, intlLocale: '', }); const ReducerRecord = ImmutableRecord({ - items: ImmutableOrderedMap(), + items: ImmutableOrderedMap(), hasMore: true, top: false, unread: 0, @@ -54,103 +52,103 @@ type QueuedNotification = ReturnType; const parseId = (id: string | number) => parseInt(id as string, 10); // For sorting the notifications -const comparator = (a: Pick, b: Pick) => { - const parse = (m: Pick) => parseId(m.id); +const comparator = (a: Pick, b: Pick) => { + const parse = (m: Pick) => parseId(m.group_key); if (parse(a) < parse(b)) return 1; if (parse(a) > parse(b)) return -1; return 0; }; -const minifyNotification = (notification: Notification) => { - // @ts-ignore - const minifiedNotification: { - duplicate: boolean; - account_id: string; - account_ids: string[]; - created_at: string; - id: string; - } & ( - | { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' } - | { - type: 'mention' | 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder'; - status_id: string; - } - | { - type: 'admin.report'; - report: Report; - } - | { - type: 'severed_relationships'; - relationship_severance_event: RelationshipSeveranceEvent; - } - | { - type: 'moderation_warning'; - moderation_warning: AccountWarning; - } - | { - type: 'move'; - target_id: string; - } - | { - type: 'emoji_reaction'; - emoji: string; - emoji_url: string | null; - status_id: string; - } - | { - type: 'chat_mention'; - chat_message_id: string; - } - | { - type: 'participation_accepted' | 'participation_request'; - status_id: string; - participation_message: string | null; - } - ) = { - ...omit(notification, ['account', 'accounts']), - created_at: notification.created_at, - id: notification.id, - type: notification.type, - }; +// const minifyNotification = (notification: Notification) => { +// // @ts-ignore +// const minifiedNotification: { +// duplicate: boolean; +// account_id: string; +// account_ids: string[]; +// created_at: string; +// id: string; +// } & ( +// | { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' } +// | { +// type: 'mention' | 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder'; +// status_id: string; +// } +// | { +// type: 'admin.report'; +// report: Report; +// } +// | { +// type: 'severed_relationships'; +// relationship_severance_event: RelationshipSeveranceEvent; +// } +// | { +// type: 'moderation_warning'; +// moderation_warning: AccountWarning; +// } +// | { +// type: 'move'; +// target_id: string; +// } +// | { +// type: 'emoji_reaction'; +// emoji: string; +// emoji_url: string | null; +// status_id: string; +// } +// | { +// type: 'chat_mention'; +// chat_message_id: string; +// } +// | { +// type: 'participation_accepted' | 'participation_request'; +// status_id: string; +// participation_message: string | null; +// } +// ) = { +// ...omit(notification, ['account', 'accounts']), +// created_at: notification.latest_page_notification_at, +// id: notification.id, +// type: notification.type, +// }; - // @ts-ignore - if (notification.status) minifiedNotification.status_id = notification.status.id; - // @ts-ignore - if (notification.target) minifiedNotification.target_id = notification.target.id; - // @ts-ignore - if (notification.chat_message) minifiedNotification.chat_message_id = notification.chat_message.id; +// // @ts-ignore +// if (notification.status) minifiedNotification.status_id = notification.status.id; +// // @ts-ignore +// if (notification.target) minifiedNotification.target_id = notification.target.id; +// // @ts-ignore +// if (notification.chat_message) minifiedNotification.chat_message_id = notification.chat_message.id; - return minifiedNotification; -}; +// return minifiedNotification; +// }; -type MinifiedNotification = ReturnType; +// type MinifiedNotification = ReturnType; // Count how many notifications appear after the given ID (for unread count) -const countFuture = (notifications: ImmutableOrderedMap, lastId: string | number) => +const countFuture = (notifications: ImmutableOrderedMap, lastId: string | number) => notifications.reduce((acc, notification) => { - if (!notification.duplicate && parseId(notification.id) > parseId(lastId)) { + if (parseId(notification.group_key) > parseId(lastId)) { return acc + 1; } else { return acc; } }, 0); -const importNotification = (state: State, notification: Notification) => { +const importNotification = (state: State, notification: NotificationGroup) => { const top = state.top; - if (!top && !notification.duplicate) state = state.update('unread', unread => unread + 1); + if (!top) state = state.update('unread', unread => unread + 1); return state.update('items', map => { if (top && map.size > 40) { map = map.take(20); } - return map.set(notification.id, minifyNotification(notification)).sort(comparator); + return map.set(notification.group_key, notification).sort(comparator); }); }; -const expandNormalizedNotifications = (state: State, notifications: Notification[], next: (() => Promise>) | null) => { - const items = ImmutableOrderedMap(notifications.map(minifyNotification).map(n => [n.id, n])); +const expandNormalizedNotifications = (state: State, notifications: NotificationGroup[], next: (() => Promise>) | null) => { + const items = ImmutableOrderedMap(notifications.map(n => [n.group_key, n])); return state.withMutations(mutable => { mutable.update('items', map => map.merge(items).sort(comparator)); @@ -161,10 +159,10 @@ const expandNormalizedNotifications = (state: State, notifications: Notification }; const filterNotifications = (state: State, relationship: Relationship) => - state.update('items', map => map.filterNot(item => item !== null && item.account_ids.includes(relationship.id))); + state.update('items', map => map.filterNot(item => item !== null && item.sample_account_ids.includes(relationship.id))); const filterNotificationIds = (state: State, accountIds: Array, type?: string) => { - const helper = (list: ImmutableOrderedMap) => list.filterNot(item => item !== null && accountIds.includes(item.account_ids[0]) && (type === undefined || type === item.type)); + const helper = (list: ImmutableOrderedMap) => list.filterNot(item => item !== null && accountIds.includes(item.sample_account_ids[0]) && (type === undefined || type === item.type)); return state.update('items', helper); }; @@ -177,19 +175,19 @@ const deleteByStatus = (state: State, statusId: string) => // @ts-ignore state.update('items', map => map.filterNot(item => item !== null && item.status === statusId)); -const updateNotificationsQueue = (state: State, notification: BaseNotification, intlMessages: Record, intlLocale: string) => { +const updateNotificationsQueue = (state: State, notification: NotificationGroup, intlMessages: Record, intlLocale: string) => { const queuedNotifications = state.queuedNotifications; const listedNotifications = state.items; const totalQueuedNotificationsCount = state.totalQueuedNotificationsCount; - const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id); + const alreadyExists = queuedNotifications.has(notification.group_key) || listedNotifications.has(notification.group_key); if (alreadyExists) return state; const newQueuedNotifications = queuedNotifications; return state.withMutations(mutable => { if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) { - mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, QueuedNotificationRecord({ + mutable.set('queuedNotifications', newQueuedNotifications.set(notification.group_key, QueuedNotificationRecord({ notification, intlMessages, intlLocale, @@ -259,7 +257,4 @@ const notifications = (state: State = ReducerRecord(), action: AnyAction | Timel } }; -export { - notifications as default, - type MinifiedNotification, -}; +export { notifications as default }; diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 868656944..96a806d6c 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -13,12 +13,10 @@ import { validId } from 'pl-fe/utils/auth'; import ConfigDB from 'pl-fe/utils/config-db'; import { shouldFilter } from 'pl-fe/utils/timelines'; -import type { Account as BaseAccount, Filter, MediaAttachment, Relationship } from 'pl-api'; +import type { Account as BaseAccount, Filter, MediaAttachment, NotificationGroup, Relationship } from 'pl-api'; import type { EntityStore } from 'pl-fe/entity-store/types'; import type { Account } from 'pl-fe/normalizers/account'; import type { Group } from 'pl-fe/normalizers/group'; -import type { Notification } from 'pl-fe/normalizers/notification'; -import type { MinifiedNotification } from 'pl-fe/reducers/notifications'; import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; import type { MRFSimple } from 'pl-fe/schemas/pleroma'; import type { RootState } from 'pl-fe/store'; @@ -27,7 +25,9 @@ const selectAccount = (state: RootState, accountId: string) => state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined; const selectAccounts = (state: RootState, accountIds: Array) => - accountIds.map(accountId => state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined); + accountIds + .map(accountId => state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined) + .filter((account): account is Account => account !== undefined); const selectOwnAccount = (state: RootState) => { if (state.me) { @@ -177,23 +177,16 @@ const makeGetStatus = () => createSelector( type SelectedStatus = Exclude>, null>; const makeGetNotification = () => createSelector([ - (_state: RootState, notification: MinifiedNotification) => notification, + (_state: RootState, notification: NotificationGroup) => notification, // @ts-ignore - (state: RootState, notification: MinifiedNotification) => selectAccount(state, notification.account_id), + (state: RootState, notification: NotificationGroup) => selectAccount(state, notification.target_id), // @ts-ignore - (state: RootState, notification: MinifiedNotification) => selectAccount(state, notification.target_id), - // @ts-ignore - (state: RootState, notification: MinifiedNotification) => state.statuses.get(notification.status_id), - (state: RootState, notification: MinifiedNotification) => notification.account_ids ? selectAccounts(state, notification.account_ids) : null, -], (notification, account, target, status, accounts): MinifiedNotification & Notification => ({ + (state: RootState, notification: NotificationGroup) => state.statuses.get(notification.status_id), + (state: RootState, notification: NotificationGroup) => selectAccounts(state, notification.sample_account_ids), +], (notification, target, status, accounts) => ({ ...notification, - // @ts-ignore - account: account || null, - // @ts-ignore - target: target || null, - // @ts-ignore - status: status || null, - // @ts-ignore + target, + status, accounts, }));