From 14ecf62f4eedb26ac15d8cee55df01f501589d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 28 Sep 2024 22:10:31 +0200 Subject: [PATCH] WIP hooks migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- packages/pl-fe/package.json | 6 +- packages/pl-fe/src/actions/accounts.ts | 37 +++--- packages/pl-fe/src/actions/directory.ts | 7 +- packages/pl-fe/src/actions/events.ts | 11 +- .../pl-fe/src/actions/familiar-followers.ts | 4 +- packages/pl-fe/src/actions/groups.ts | 6 +- packages/pl-fe/src/actions/history.ts | 5 +- packages/pl-fe/src/actions/importer/index.ts | 24 ++-- packages/pl-fe/src/actions/interactions.ts | 32 ++--- .../src/features/account-timeline/index.tsx | 2 +- .../hooks/notifications/useNotifications.ts | 70 +++++++---- packages/pl-fe/src/pl-hooks/importer.ts | 114 +++++++++++++++++- packages/pl-fe/src/queries/chats.ts | 10 +- packages/pl-fe/yarn.lock | 35 +++--- 14 files changed, 249 insertions(+), 114 deletions(-) diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 134ad20ce..c558fb1b3 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -60,7 +60,7 @@ "@reach/popover": "^0.18.0", "@reach/rect": "^0.18.0", "@reach/tabs": "^0.18.0", - "@reduxjs/toolkit": "^2.0.1", + "@reduxjs/toolkit": "^2.2.7", "@sentry/browser": "^7.74.1", "@sentry/react": "^7.74.1", "@tabler/icons": "^3.18.0", @@ -121,7 +121,7 @@ "react-inlinesvg": "^4.0.0", "react-intl": "^6.7.0", "react-motion": "^0.5.2", - "react-redux": "^9.0.4", + "react-redux": "^9.1.2", "react-router-dom": "^5.3.4", "react-router-dom-v5-compat": "^6.24.1", "react-router-scroll-4": "^1.0.0-beta.2", @@ -129,7 +129,7 @@ "react-sparklines": "^1.7.0", "react-sticky-box": "^2.0.0", "react-swipeable-views": "^0.14.0", - "redux": "^5.0.0", + "redux": "^5.0.1", "redux-immutable": "^4.0.0", "redux-thunk": "^3.1.0", "reselect": "^5.0.0", diff --git a/packages/pl-fe/src/actions/accounts.ts b/packages/pl-fe/src/actions/accounts.ts index e0f27c4cd..0f8e72805 100644 --- a/packages/pl-fe/src/actions/accounts.ts +++ b/packages/pl-fe/src/actions/accounts.ts @@ -1,14 +1,11 @@ import { PLEROMA, type UpdateNotificationSettingsParams, type Account, type CreateAccountParams, type PaginatedResponse, type Relationship } from 'pl-api'; -import { importEntities } from 'pl-fe/entity-store/actions'; -import { Entities } from 'pl-fe/entity-store/entities'; +import { importEntities } from 'pl-fe/pl-hooks/importer'; import { selectAccount } from 'pl-fe/selectors'; import { isLoggedIn } from 'pl-fe/utils/auth'; import { getClient, type PlfeResponse } from '../api'; -import { importFetchedAccount, importFetchedAccounts } from './importer'; - import type { Map as ImmutableMap } from 'immutable'; import type { MinifiedStatus } from 'pl-fe/reducers/statuses'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -128,7 +125,7 @@ const fetchAccount = (accountId: string) => return getClient(getState()).accounts.getAccount(accountId) .then(response => { - dispatch(importFetchedAccount(response)); + importEntities({ accounts: [response] }); dispatch(fetchAccountSuccess(response)); }) .catch(error => { @@ -143,8 +140,8 @@ const fetchAccountByUsername = (username: string, history?: History) => if (features.accountByUsername && (me || !features.accountLookup)) { return getClient(getState()).accounts.getAccount(username).then(response => { + importEntities({ accounts: [response] }); dispatch(fetchRelationships([response.id])); - dispatch(importFetchedAccount(response)); dispatch(fetchAccountSuccess(response)); }).catch(error => { dispatch(fetchAccountFail(null, error)); @@ -198,7 +195,7 @@ const blockAccount = (accountId: string) => return getClient(getState).filtering.blockAccount(accountId) .then(response => { - dispatch(importEntities([response], Entities.RELATIONSHIPS)); + importEntities({ relationships: [response] }); // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers return dispatch(blockAccountSuccess(response, getState().statuses)); }).catch(error => dispatch(blockAccountFail(error))); @@ -212,7 +209,7 @@ const unblockAccount = (accountId: string) => return getClient(getState).filtering.unblockAccount(accountId) .then(response => { - dispatch(importEntities([response], Entities.RELATIONSHIPS)); + importEntities({ relationships: [response] }); return dispatch(unblockAccountSuccess(response)); }) .catch(error => dispatch(unblockAccountFail(error))); @@ -273,7 +270,7 @@ const muteAccount = (accountId: string, notifications?: boolean, duration = 0) = return client.filtering.muteAccount(accountId, params) .then(response => { - dispatch(importEntities([response], Entities.RELATIONSHIPS)); + importEntities({ relationships: [response] }); // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers return dispatch(muteAccountSuccess(response, getState().statuses)); }) @@ -288,7 +285,7 @@ const unmuteAccount = (accountId: string) => return getClient(getState()).filtering.unmuteAccount(accountId) .then(response => { - dispatch(importEntities([response], Entities.RELATIONSHIPS)); + importEntities({ relationships: [response] }); return dispatch(unmuteAccountSuccess(response)); }) .catch(error => dispatch(unmuteAccountFail(accountId, error))); @@ -369,7 +366,7 @@ const fetchRelationships = (accountIds: string[]) => return getClient(getState()).accounts.getRelationships(newAccountIds) .then(response => { - dispatch(importEntities(response, Entities.RELATIONSHIPS)); + importEntities({ relationships: response }); dispatch(fetchRelationshipsSuccess(response)); }) .catch(error => dispatch(fetchRelationshipsFail(error))); @@ -398,7 +395,7 @@ const fetchFollowRequests = () => return getClient(getState()).myAccount.getFollowRequests() .then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); dispatch(fetchFollowRequestsSuccess(response.items, response.next)); }) .catch(error => dispatch(fetchFollowRequestsFail(error))); @@ -430,7 +427,7 @@ const expandFollowRequests = () => dispatch(expandFollowRequestsRequest()); return next().then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); dispatch(expandFollowRequestsSuccess(response.items, response.next)); }).catch(error => dispatch(expandFollowRequestsFail(error))); }; @@ -576,7 +573,7 @@ const fetchPinnedAccounts = (accountId: string) => dispatch(fetchPinnedAccountsRequest(accountId)); return getClient(getState).accounts.getAccountEndorsements(accountId).then(response => { - dispatch(importFetchedAccounts(response)); + importEntities({ accounts: response }); dispatch(fetchPinnedAccountsSuccess(accountId, response, null)); }).catch(error => { dispatch(fetchPinnedAccountsFail(accountId, error)); @@ -604,10 +601,10 @@ const fetchPinnedAccountsFail = (accountId: string, error: unknown) => ({ const accountSearch = (q: string, signal?: AbortSignal) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ACCOUNT_SEARCH_REQUEST, params: { q } }); - return getClient(getState()).accounts.searchAccounts(q, { resolve: false, limit: 4, following: true }, { signal }).then((accounts) => { - dispatch(importFetchedAccounts(accounts)); - dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts }); - return accounts; + return getClient(getState()).accounts.searchAccounts(q, { resolve: false, limit: 4, following: true }, { signal }).then((response) => { + importEntities({ accounts: response }); + dispatch({ type: ACCOUNT_SEARCH_SUCCESS, accounts: response }); + return response; }).catch(error => { dispatch({ type: ACCOUNT_SEARCH_FAIL, skipAlert: true }); throw error; @@ -618,7 +615,7 @@ const accountLookup = (acct: string, signal?: AbortSignal) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: ACCOUNT_LOOKUP_REQUEST, acct }); return getClient(getState()).accounts.lookupAccount(acct, { signal }).then((account) => { - if (account && account.id) dispatch(importFetchedAccount(account)); + if (account && account.id) importEntities({ accounts: [account] }); dispatch({ type: ACCOUNT_LOOKUP_SUCCESS, account }); return account; }).catch(error => { @@ -636,7 +633,7 @@ const fetchBirthdayReminders = (month: number, day: number) => dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, accountId: me }); return getClient(getState).accounts.getBirthdays(day, month).then(response => { - dispatch(importFetchedAccounts(response)); + importEntities({ accounts: response }); dispatch({ type: BIRTHDAY_REMINDERS_FETCH_SUCCESS, accounts: response, diff --git a/packages/pl-fe/src/actions/directory.ts b/packages/pl-fe/src/actions/directory.ts index 14a5e8151..4fecad216 100644 --- a/packages/pl-fe/src/actions/directory.ts +++ b/packages/pl-fe/src/actions/directory.ts @@ -1,7 +1,8 @@ +import { importEntities } from 'pl-fe/pl-hooks/importer'; + import { getClient } from '../api'; import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; import type { Account, ProfileDirectoryParams } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -19,7 +20,7 @@ const fetchDirectory = (params: ProfileDirectoryParams) => dispatch(fetchDirectoryRequest()); return getClient(getState()).instance.profileDirectory({ ...params, limit: 20 }).then((data) => { - dispatch(importFetchedAccounts(data)); + importEntities({ accounts: data }); dispatch(fetchDirectorySuccess(data)); dispatch(fetchRelationships(data.map((x) => x.id))); }).catch(error => dispatch(fetchDirectoryFail(error))); @@ -46,7 +47,7 @@ const expandDirectory = (params: Record) => const loadedItems = getState().user_lists.directory.items.size; return getClient(getState()).instance.profileDirectory({ ...params, offset: loadedItems, limit: 20 }).then((data) => { - dispatch(importFetchedAccounts(data)); + importEntities({ accounts: data }); dispatch(expandDirectorySuccess(data)); dispatch(fetchRelationships(data.map((x) => x.id))); }).catch(error => dispatch(expandDirectoryFail(error))); diff --git a/packages/pl-fe/src/actions/events.ts b/packages/pl-fe/src/actions/events.ts index 984ffd68a..1ac8520db 100644 --- a/packages/pl-fe/src/actions/events.ts +++ b/packages/pl-fe/src/actions/events.ts @@ -1,10 +1,11 @@ import { defineMessages } from 'react-intl'; import { getClient } from 'pl-fe/api'; +import { importEntities } from 'pl-fe/pl-hooks/importer'; import { useModalsStore } from 'pl-fe/stores'; import toast from 'pl-fe/toast'; -import { importFetchedAccounts, importFetchedStatus, importFetchedStatuses } from './importer'; +import { importFetchedStatus, importFetchedStatuses } from './importer'; import { STATUS_FETCH_SOURCE_FAIL, STATUS_FETCH_SOURCE_REQUEST, STATUS_FETCH_SOURCE_SUCCESS } from './statuses'; import type { Account, CreateEventParams, Location, MediaAttachment, PaginatedResponse, Status } from 'pl-api'; @@ -241,7 +242,7 @@ const fetchEventParticipations = (statusId: string) => dispatch(fetchEventParticipationsRequest(statusId)); return getClient(getState).events.getEventParticipations(statusId).then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); return dispatch(fetchEventParticipationsSuccess(statusId, response.items, response.next)); }).catch(error => { dispatch(fetchEventParticipationsFail(statusId, error)); @@ -277,7 +278,7 @@ const expandEventParticipations = (statusId: string) => dispatch(expandEventParticipationsRequest(statusId)); return next().then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); return dispatch(expandEventParticipationsSuccess(statusId, response.items, response.next)); }).catch(error => { dispatch(expandEventParticipationsFail(statusId, error)); @@ -307,7 +308,7 @@ const fetchEventParticipationRequests = (statusId: string) => dispatch(fetchEventParticipationRequestsRequest(statusId)); return getClient(getState).events.getEventParticipationRequests(statusId).then(response => { - dispatch(importFetchedAccounts(response.items.map(({ account }) => account))); + importEntities({ accounts: response.items.map(({ account }) => account) }); return dispatch(fetchEventParticipationRequestsSuccess(statusId, response.items, response.next)); }).catch(error => { dispatch(fetchEventParticipationRequestsFail(statusId, error)); @@ -346,7 +347,7 @@ const expandEventParticipationRequests = (statusId: string) => dispatch(expandEventParticipationRequestsRequest(statusId)); return next().then(response => { - dispatch(importFetchedAccounts(response.items.map(({ account }) => account))); + importEntities({ accounts: response.items.map(({ account }) => account) }); return dispatch(expandEventParticipationRequestsSuccess(statusId, response.items, response.next)); }).catch(error => { dispatch(expandEventParticipationRequestsFail(statusId, error)); diff --git a/packages/pl-fe/src/actions/familiar-followers.ts b/packages/pl-fe/src/actions/familiar-followers.ts index 12bf81f83..d314f9128 100644 --- a/packages/pl-fe/src/actions/familiar-followers.ts +++ b/packages/pl-fe/src/actions/familiar-followers.ts @@ -1,9 +1,9 @@ +import { importEntities } from 'pl-fe/pl-hooks/importer'; import { AppDispatch, RootState } from 'pl-fe/store'; import { getClient } from '../api'; import { fetchRelationships } from './accounts'; -import { importFetchedAccounts } from './importer'; const FAMILIAR_FOLLOWERS_FETCH_REQUEST = 'FAMILIAR_FOLLOWERS_FETCH_REQUEST' as const; const FAMILIAR_FOLLOWERS_FETCH_SUCCESS = 'FAMILIAR_FOLLOWERS_FETCH_SUCCESS' as const; @@ -19,7 +19,7 @@ const fetchAccountFamiliarFollowers = (accountId: string) => (dispatch: AppDispa .then((data) => { const accounts = data.find(({ id }: { id: string }) => id === accountId)!.accounts; - dispatch(importFetchedAccounts(accounts)); + importEntities({ accounts }); dispatch(fetchRelationships(accounts.map((item) => item.id))); dispatch({ type: FAMILIAR_FOLLOWERS_FETCH_SUCCESS, diff --git a/packages/pl-fe/src/actions/groups.ts b/packages/pl-fe/src/actions/groups.ts index ed201388a..fc8b469db 100644 --- a/packages/pl-fe/src/actions/groups.ts +++ b/packages/pl-fe/src/actions/groups.ts @@ -1,6 +1,6 @@ -import { getClient } from '../api'; +import { importEntities } from 'pl-fe/pl-hooks/importer'; -import { importFetchedAccounts } from './importer'; +import { getClient } from '../api'; import type { Account, PaginatedResponse } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -23,7 +23,7 @@ const fetchGroupBlocks = (groupId: string) => dispatch(fetchGroupBlocksRequest(groupId)); return getClient(getState).experimental.groups.getGroupBlocks(groupId).then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); dispatch(fetchGroupBlocksSuccess(groupId, response.items, response.next)); }).catch(error => { dispatch(fetchGroupBlocksFail(groupId, error)); diff --git a/packages/pl-fe/src/actions/history.ts b/packages/pl-fe/src/actions/history.ts index 733871b17..1f110f11e 100644 --- a/packages/pl-fe/src/actions/history.ts +++ b/packages/pl-fe/src/actions/history.ts @@ -1,6 +1,5 @@ import { getClient } from 'pl-fe/api'; - -import { importFetchedAccounts } from './importer'; +import { importEntities } from 'pl-fe/pl-hooks/importer'; import type { StatusEdit } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -18,7 +17,7 @@ const fetchHistory = (statusId: string) => dispatch(fetchHistoryRequest(statusId)); return getClient(getState()).statuses.getStatusHistory(statusId).then(data => { - dispatch(importFetchedAccounts(data.map((x) => x.account))); + importEntities({ accounts: data.map((x) => x.account) }); dispatch(fetchHistorySuccess(statusId, data)); }).catch(error => dispatch(fetchHistoryFail(statusId, error))); }; diff --git a/packages/pl-fe/src/actions/importer/index.ts b/packages/pl-fe/src/actions/importer/index.ts index ed63ddb13..b205cc4c4 100644 --- a/packages/pl-fe/src/actions/importer/index.ts +++ b/packages/pl-fe/src/actions/importer/index.ts @@ -1,8 +1,8 @@ import { importEntities } from 'pl-fe/entity-store/actions'; import { Entities } from 'pl-fe/entity-store/entities'; -import { normalizeAccount, normalizeGroup } from 'pl-fe/normalizers'; +import { normalizeAccount, normalizeGroup, type Account, type Group } from 'pl-fe/normalizers'; -import type { Account as BaseAccount, Group, Poll, Status as BaseStatus } from 'pl-api'; +import type { Account as BaseAccount, Group as BaseGroup, Poll, Status as BaseStatus } from 'pl-api'; import type { AppDispatch } from 'pl-fe/store'; const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; @@ -13,25 +13,29 @@ const POLLS_IMPORT = 'POLLS_IMPORT'; const importAccount = (data: BaseAccount) => importAccounts([data]); -const importAccounts = (data: Array) => (dispatch: AppDispatch) => { - dispatch({ type: ACCOUNTS_IMPORT, accounts: data }); +const importAccounts = (data: Array) => { + let accounts: Array = []; + try { - const accounts = data.map(normalizeAccount); - dispatch(importEntities(accounts, Entities.ACCOUNTS)); + accounts = data.map(normalizeAccount); } catch (e) { // } + + return importEntities(accounts, Entities.ACCOUNTS); }; -const importGroup = (data: Group) => importGroups([data]); +const importGroup = (data: BaseGroup) => importGroups([data]); -const importGroups = (data: Array) => (dispatch: AppDispatch) => { +const importGroups = (data: Array) => { + let groups: Array = []; try { - const groups = data.map(normalizeGroup); - dispatch(importEntities(groups, Entities.GROUPS)); + groups = data.map(normalizeGroup); } catch (e) { // } + + return importEntities(groups, Entities.GROUPS); }; const importStatus = (status: BaseStatus & { expectsCard?: boolean }, idempotencyKey?: string) => ({ type: STATUS_IMPORT, status, idempotencyKey }); diff --git a/packages/pl-fe/src/actions/interactions.ts b/packages/pl-fe/src/actions/interactions.ts index 5bc7f1dcd..2473768e2 100644 --- a/packages/pl-fe/src/actions/interactions.ts +++ b/packages/pl-fe/src/actions/interactions.ts @@ -1,5 +1,6 @@ import { defineMessages } from 'react-intl'; +import { importEntities } from 'pl-fe/pl-hooks/importer'; import { useModalsStore } from 'pl-fe/stores'; import toast, { type IToastOptions } from 'pl-fe/toast'; import { isLoggedIn } from 'pl-fe/utils/auth'; @@ -7,7 +8,6 @@ import { isLoggedIn } from 'pl-fe/utils/auth'; import { getClient } from '../api'; import { fetchRelationships } from './accounts'; -import { importFetchedAccounts, importFetchedStatus } from './importer'; import type { Account, EmojiReaction, PaginatedResponse, Status } from 'pl-api'; import type { AppDispatch, RootState } from 'pl-fe/store'; @@ -97,7 +97,7 @@ const reblog = (status: Pick) => return getClient(getState()).statuses.reblogStatus(status.id).then((response) => { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper - if (response.reblog) dispatch(importFetchedStatus(response.reblog as Status)); + if (response.reblog) importEntities({ statuses: [response] }); dispatch(reblogSuccess(response)); }).catch(error => { dispatch(reblogFail(status.id, error)); @@ -110,8 +110,8 @@ const unreblog = (status: Pick) => dispatch(unreblogRequest(status.id)); - return getClient(getState()).statuses.unreblogStatus(status.id).then((status) => { - dispatch(unreblogSuccess(status)); + return getClient(getState()).statuses.unreblogStatus(status.id).then((response) => { + dispatch(unreblogSuccess(response)); }).catch(error => { dispatch(unreblogFail(status.id, error)); }); @@ -306,9 +306,9 @@ const bookmark = (status: Pick, folderId?: string) => dispatch(bookmarkRequest(status.id)); - return getClient(getState()).statuses.bookmarkStatus(status.id, folderId).then((response) => { - dispatch(importFetchedStatus(response)); - dispatch(bookmarkSuccess(response)); + return getClient(getState()).statuses.bookmarkStatus(status.id, folderId).then((status) => { + importEntities({ statuses: [status] }); + dispatch(bookmarkSuccess(status)); let opts: IToastOptions = { actionLabel: messages.view, @@ -335,7 +335,7 @@ const unbookmark = (status: Pick) => dispatch(unbookmarkRequest(status.id)); return getClient(getState()).statuses.unbookmarkStatus(status.id).then(response => { - dispatch(importFetchedStatus(response)); + importEntities({ statuses: [response] }); dispatch(unbookmarkSuccess(response)); toast.success(messages.bookmarkRemoved); }).catch(error => { @@ -391,7 +391,7 @@ const fetchReblogs = (statusId: string) => dispatch(fetchReblogsRequest(statusId)); return getClient(getState()).statuses.getRebloggedBy(statusId).then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); dispatch(fetchRelationships(response.items.map((item) => item.id))); dispatch(fetchReblogsSuccess(statusId, response.items, response.next)); }).catch(error => { @@ -420,7 +420,7 @@ const fetchReblogsFail = (statusId: string, error: unknown) => ({ const expandReblogs = (statusId: string, next: AccountListLink) => (dispatch: AppDispatch, getState: () => RootState) => { next().then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); dispatch(fetchRelationships(response.items.map((item) => item.id))); dispatch(expandReblogsSuccess(statusId, response.items, response.next)); }).catch(error => { @@ -446,7 +446,7 @@ const fetchFavourites = (statusId: string) => dispatch(fetchFavouritesRequest(statusId)); return getClient(getState()).statuses.getFavouritedBy(statusId).then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); dispatch(fetchRelationships(response.items.map((item) => item.id))); dispatch(fetchFavouritesSuccess(statusId, response.items, response.next)); }).catch(error => { @@ -475,7 +475,7 @@ const fetchFavouritesFail = (statusId: string, error: unknown) => ({ const expandFavourites = (statusId: string, next: AccountListLink) => (dispatch: AppDispatch) => { next().then(response => { - dispatch(importFetchedAccounts(response.items)); + importEntities({ accounts: response.items }); dispatch(fetchRelationships(response.items.map((item) => item.id))); dispatch(expandFavouritesSuccess(statusId, response.items, response.next)); }).catch(error => { @@ -501,7 +501,7 @@ const fetchDislikes = (statusId: string) => dispatch(fetchDislikesRequest(statusId)); return getClient(getState).statuses.getDislikedBy(statusId).then(response => { - dispatch(importFetchedAccounts(response)); + importEntities({ accounts: response }); dispatch(fetchRelationships(response.map((item) => item.id))); dispatch(fetchDislikesSuccess(statusId, response)); }).catch(error => { @@ -531,7 +531,7 @@ const fetchReactions = (statusId: string) => dispatch(fetchReactionsRequest(statusId)); return getClient(getState).statuses.getStatusReactions(statusId).then(response => { - dispatch(importFetchedAccounts((response).map(({ accounts }) => accounts).flat())); + importEntities({ accounts: (response).map(({ accounts }) => accounts).flat() }); dispatch(fetchReactionsSuccess(statusId, response)); }).catch(error => { dispatch(fetchReactionsFail(statusId, error)); @@ -562,7 +562,7 @@ const pin = (status: Pick, accountId: string) => dispatch(pinRequest(status.id, accountId)); return getClient(getState()).statuses.pinStatus(status.id).then(response => { - dispatch(importFetchedStatus(response)); + importEntities({ statuses: [response] }); dispatch(pinSuccess(response, accountId)); }).catch(error => { dispatch(pinFail(status.id, error, accountId)); @@ -596,7 +596,7 @@ const unpin = (status: Pick, accountId: string) => dispatch(unpinRequest(status.id, accountId)); return getClient(getState()).statuses.unpinStatus(status.id).then(response => { - dispatch(importFetchedStatus(response)); + importEntities({ statuses: [response] }); dispatch(unpinSuccess(response, accountId)); }).catch(error => { dispatch(unpinFail(status.id, error, accountId)); diff --git a/packages/pl-fe/src/features/account-timeline/index.tsx b/packages/pl-fe/src/features/account-timeline/index.tsx index 47fee24d1..fd8db60f7 100644 --- a/packages/pl-fe/src/features/account-timeline/index.tsx +++ b/packages/pl-fe/src/features/account-timeline/index.tsx @@ -34,7 +34,7 @@ const AccountTimeline: React.FC = ({ params, withReplies = fal const statusIds = useAppSelector(state => getStatusIds(state, { type: `account:${path}`, prefix: 'account_timeline' })); const featuredStatusIds = useAppSelector(state => getStatusIds(state, { type: `account:${account?.id}:with_replies:pinned`, prefix: 'account_timeline' })); - const isBlocked = useAppSelector(state => state.relationships.getIn([account?.id, 'blocked_by']) === true); + const isBlocked = account?.relationship?.blocked_by; const unavailable = isBlocked && !features.blockersVisible; const isLoading = useAppSelector(state => state.timelines.get(`account:${path}`)?.isLoading === true); const hasMore = useAppSelector(state => state.timelines.get(`account:${path}`)?.hasMore === true); diff --git a/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotifications.ts b/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotifications.ts index 47bc5f4cd..c38f3a938 100644 --- a/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotifications.ts +++ b/packages/pl-fe/src/pl-hooks/hooks/notifications/useNotifications.ts @@ -1,20 +1,16 @@ import { useInfiniteQuery } from '@tanstack/react-query'; -import { importFetchedAccounts, importFetchedStatuses } from 'pl-fe/actions/importer'; +import { getNotificationStatus } from 'pl-fe/features/notifications/components/notification'; import { useClient } from 'pl-fe/hooks'; -import { normalizeNotifications } from 'pl-fe/normalizers'; +import { importEntities } from 'pl-fe/pl-hooks/importer'; import { queryClient } from 'pl-fe/queries/client'; -import { AppDispatch, store } from 'pl-fe/store'; import { flattenPages } from 'pl-fe/utils/queries'; -import { importNotification, minifyNotification } from './useNotification'; - import type { Account as BaseAccount, Notification as BaseNotification, PaginatedResponse, PlApiClient, - Status as BaseStatus, } from 'pl-api'; import type { NotificationType } from 'pl-fe/utils/notification'; @@ -29,31 +25,59 @@ const getQueryKey = (params: UseNotificationParams) => [ params.types ? params.types.join('|') : params.excludeTypes ? ('exclude:' + params.excludeTypes.join('|')) : 'all', ]; -const importNotifications = (response: PaginatedResponse) => { - const accounts: Record = {}; - const statuses: Record = {}; +type DeduplicatedNotification = BaseNotification & { + accounts: Array; + duplicate?: boolean; +} - response.items.forEach((notification) => { - accounts[notification.account.id] = notification.account; +const STATUS_NOTIFICATION_TYPES = [ + 'favourite', + 'reblog', + 'emoji_reaction', + 'event_reminder', + 'participation_accepted', + 'participation_request', +]; - if (notification.type === 'move') accounts[notification.target.id] = notification.target; +const deduplicateNotifications = (notifications: Array) => { + const deduplicatedNotifications: DeduplicatedNotification[] = []; - // @ts-ignore - if (notification.status?.id) { - // @ts-ignore - statuses[notification.status.id] = notification.status; + for (const notification of notifications) { + 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(notification.account); + deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: true }); + } else { + deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: false }); + } + } else { + deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: false }); } + } + + return deduplicatedNotifications; +}; + +const importNotifications = (response: PaginatedResponse) => { + const deduplicatedNotifications = deduplicateNotifications(response.items); + + importEntities({ + notifications: deduplicatedNotifications, }); - store.dispatch(importFetchedStatuses(Object.values(statuses))); - store.dispatch(importFetchedAccounts(Object.values(accounts))); + // const normalizedNotifications = normalizeNotifications(response.items); - const normalizedNotifications = normalizeNotifications(response.items); - - normalizedNotifications.map(minifyNotification).forEach(importNotification); + // normalizedNotifications.map(minifyNotification).forEach(importNotification); return { - items: normalizedNotifications.filter(({ duplicate }) => !duplicate).map(({ id }) => id), + items: deduplicatedNotifications.filter(({ duplicate }) => !duplicate).map(({ id }) => id), previous: response.previous, next: response.next, }; @@ -91,4 +115,4 @@ const prefetchNotifications = (client: PlApiClient, params: UseNotificationParam getNextPageParam: (response) => response, }); -export { useNotifications, prefetchNotifications }; +export { useNotifications, prefetchNotifications, type DeduplicatedNotification }; diff --git a/packages/pl-fe/src/pl-hooks/importer.ts b/packages/pl-fe/src/pl-hooks/importer.ts index 6f4105dc5..84f56786e 100644 --- a/packages/pl-fe/src/pl-hooks/importer.ts +++ b/packages/pl-fe/src/pl-hooks/importer.ts @@ -1,28 +1,125 @@ +import omit from 'lodash/omit'; + +import { importAccounts, importGroups, importPolls, importStatuses } from 'pl-fe/actions/importer'; +import { importEntities as importEntityStoreEntities } from 'pl-fe/entity-store/actions'; +import { Entities } from 'pl-fe/entity-store/entities'; +import { queryClient } from 'pl-fe/queries/client'; +import { store } from 'pl-fe/store'; + +import { DeduplicatedNotification } from './hooks/notifications/useNotifications'; + import type { + AccountWarning, Account as BaseAccount, Group as BaseGroup, - Notification as BaseNotification, Poll as BasePoll, + Relationship as BaseRelationship, Status as BaseStatus, + RelationshipSeveranceEvent, } from 'pl-api'; + +const minifyNotification = (notification: DeduplicatedNotification) => { + // @ts-ignore + const minifiedNotification: { + duplicate: boolean; + account_id: string; + account_ids: string[]; + created_at: string; + id: string; + group_key: string; + } & ( + | { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' } + | { + type: 'mention'; + subtype?: 'reply'; + status_id: string; + } + | { + type: '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', 'status', 'target', 'chat_message']), + account_id: notification.account.id, + account_ids: notification.accounts.map(({ id }) => id), + created_at: notification.created_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; + + return minifiedNotification; +}; + +type MinifiedNotification = ReturnType; + +const importNotification = (notification: DeduplicatedNotification) => { + queryClient.setQueryData( + ['notifications', 'entities', notification.id], + existingNotification => existingNotification?.duplicate ? existingNotification : minifyNotification(notification), + ); +}; + const importEntities = (entities: { accounts?: Array; - notifications?: Array; + notifications?: Array; statuses?: Array; + relationships?: Array; }) => { + const { dispatch } = store; + const accounts: Record = {}; const groups: Record = {}; - const notifications: Record = {}; + const notifications: Record = {}; const polls: Record = {}; + const relationships: Record = {}; const statuses: Record = {}; const processAccount = (account: BaseAccount) => { accounts[account.id] = account; if (account.moved) processAccount(account.moved); + if (account.relationship) relationships[account.relationship.id] = account.relationship; }; - const processNotification = (notification: BaseNotification) => { + const processNotification = (notification: DeduplicatedNotification) => { notifications[notification.id] = notification; processAccount(notification.account); @@ -38,11 +135,20 @@ const importEntities = (entities: { if (status.quote) processStatus(status.quote); if (status.reblog) processStatus(status.reblog); + if (status.poll) polls[status.poll.id] = status.poll; + if (status.group) groups[status.group.id] = status.group; }; entities.accounts?.forEach(processAccount); entities.notifications?.forEach(processNotification); entities.statuses?.forEach(processStatus); + + dispatch(importAccounts(Object.values(accounts))); + dispatch(importGroups(Object.values(groups))); + Object.values(notifications).forEach(importNotification); + dispatch(importPolls(Object.values(polls))); + dispatch(importStatuses(Object.values(statuses))); + dispatch(importEntityStoreEntities(Object.values(relationships), Entities.RELATIONSHIPS)); }; export { importEntities }; diff --git a/packages/pl-fe/src/queries/chats.ts b/packages/pl-fe/src/queries/chats.ts index 1aa00f812..6b1f08c71 100644 --- a/packages/pl-fe/src/queries/chats.ts +++ b/packages/pl-fe/src/queries/chats.ts @@ -2,11 +2,11 @@ import { InfiniteData, keepPreviousData, useInfiniteQuery, useMutation, useQuery import sumBy from 'lodash/sumBy'; import { type Chat, type ChatMessage as BaseChatMessage, type PaginatedResponse, chatMessageSchema } from 'pl-api'; -import { importFetchedAccount, importFetchedAccounts } from 'pl-fe/actions/importer'; import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context'; import { useStatContext } from 'pl-fe/contexts/stat-context'; -import { useAppDispatch, useAppSelector, useClient, useFeatures, useLoggedIn, useOwnAccount } from 'pl-fe/hooks'; +import { useAppSelector, useClient, useFeatures, useLoggedIn, useOwnAccount } from 'pl-fe/hooks'; import { type ChatMessage, normalizeChatMessage } from 'pl-fe/normalizers'; +import { importEntities } from 'pl-fe/pl-hooks/importer'; import { reOrderChatListItems } from 'pl-fe/utils/chats'; import { flattenPages, updatePageItem } from 'pl-fe/utils/queries'; @@ -51,7 +51,6 @@ const useChatMessages = (chat: Chat) => { const useChats = () => { const client = useClient(); - const dispatch = useAppDispatch(); const features = useFeatures(); const { setUnreadChatsCount } = useStatContext(); const fetchRelationships = useFetchRelationships(); @@ -65,7 +64,7 @@ const useChats = () => { // Set the relationships to these users in the redux store. fetchRelationships.mutate({ accountIds: items.map((item) => item.account.id) }); - dispatch(importFetchedAccounts(items.map((item) => item.account))); + importEntities({ accounts: items.map((item) => item.account) }); return response; }; @@ -94,7 +93,6 @@ const useChats = () => { const useChat = (chatId?: string) => { const client = useClient(); - const dispatch = useAppDispatch(); const fetchRelationships = useFetchRelationships(); const getChat = async () => { @@ -102,7 +100,7 @@ const useChat = (chatId?: string) => { const data = await client.chats.getChat(chatId); fetchRelationships.mutate({ accountIds: [data.account.id] }); - dispatch(importFetchedAccount(data.account)); + importEntities({ accounts: [data.account] }); return data; } diff --git a/packages/pl-fe/yarn.lock b/packages/pl-fe/yarn.lock index a937fcc2c..4375ddf75 100644 --- a/packages/pl-fe/yarn.lock +++ b/packages/pl-fe/yarn.lock @@ -2171,15 +2171,15 @@ resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee" integrity sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A== -"@reduxjs/toolkit@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.0.1.tgz#0a5233c1e35c1941b03aece39cceade3467a1062" - integrity sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA== +"@reduxjs/toolkit@^2.2.7": + version "2.2.7" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-2.2.7.tgz#199e3d10ccb39267cb5aee92c0262fd9da7fdfb2" + integrity sha512-faI3cZbSdFb8yv9dhDTmGwclW0vk0z5o1cia+kf7gCbaCwHI5e+7tP57mJUv22pNcNbeA62GSrPpfrUfdXcQ6g== dependencies: immer "^10.0.3" - redux "^5.0.0" + redux "^5.0.1" redux-thunk "^3.1.0" - reselect "^5.0.1" + reselect "^5.1.0" "@remix-run/router@1.18.0": version "1.18.0" @@ -9713,10 +9713,10 @@ react-property@2.0.2: resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.2.tgz#d5ac9e244cef564880a610bc8d868bd6f60fdda6" integrity sha512-+PbtI3VuDV0l6CleQMsx2gtK0JZbZKbpdu5ynr+lbsuvtmgbNcS3VM0tuY2QjFNOcWxvXeHjDpy42RO+4U2rug== -react-redux@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.0.4.tgz#6892d465f086507a517d4b53eb589876e6bc8344" - integrity sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA== +react-redux@^9.1.2: + version "9.1.2" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-9.1.2.tgz#deba38c64c3403e9abd0c3fbeab69ffd9d8a7e4b" + integrity sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w== dependencies: "@types/use-sync-external-store" "^0.0.3" use-sync-external-store "^1.0.0" @@ -9908,10 +9908,10 @@ redux@^4.0.5: dependencies: "@babel/runtime" "^7.9.2" -redux@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.0.tgz#29572e29a439e094ff8fec46883fc45053f6736d" - integrity sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA== +redux@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-5.0.1.tgz#97fa26881ce5746500125585d5642c77b6e9447b" + integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== reflect.getprototypeof@^1.0.4: version "1.0.4" @@ -10051,11 +10051,16 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -reselect@^5.0.0, reselect@^5.0.1: +reselect@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.0.1.tgz#587cdaaeb4e0e8927cff80ebe2bbef05f74b1648" integrity sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg== +reselect@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" + integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"