diff --git a/package-lock.json b/package-lock.json index 3013754..4aae095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@internxt/css-config": "^1.1.0", "@internxt/lib": "^1.4.1", "@internxt/sdk": "^1.15.7", - "@internxt/ui": "^0.1.11", + "@internxt/ui": "^0.1.12", "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.2.1", @@ -1214,9 +1214,9 @@ } }, "node_modules/@internxt/ui": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/@internxt/ui/-/ui-0.1.11.tgz", - "integrity": "sha512-TxxRUcEw+EoHnfkmBwJgmENqkYqz8NjQtYxzgNCk2cD+BMzREXv1c57fLYhq4jiGQIISb3HauI53p32WDN+bPg==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@internxt/ui/-/ui-0.1.12.tgz", + "integrity": "sha512-ADhUIyEb0KQT3EAUHPmzyVVIiwhHgCuYykyv7ZNc7XUVC2nFu47FuP99qlieTh11/xB2q4Lfh4ul9+keLTYVug==", "license": "MIT", "dependencies": { "@internxt/css-config": "1.1.0", diff --git a/package.json b/package.json index 130ff89..2c1c3a0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@internxt/css-config": "^1.1.0", "@internxt/lib": "^1.4.1", "@internxt/sdk": "^1.15.7", - "@internxt/ui": "^0.1.11", + "@internxt/ui": "^0.1.12", "@phosphor-icons/react": "^2.1.10", "@reduxjs/toolkit": "^2.11.2", "@tailwindcss/vite": "^4.2.1", diff --git a/src/features/mail/MailView.tsx b/src/features/mail/MailView.tsx index 68cb10c..597c768 100644 --- a/src/features/mail/MailView.tsx +++ b/src/features/mail/MailView.tsx @@ -17,7 +17,7 @@ const MailView = ({ folder }: MailViewProps) => { const { translate } = useTranslationContext(); const [activeMailId, setActiveMailId] = useState(undefined); - const { isLoadingListFolder, listFolder, hasMore: hasMoreItems, onLoadMore } = useListFolderPaginated(folder); + const { isLoadingListFolder, listFolderEmails, hasMoreEmails, onLoadMore } = useListFolderPaginated(folder); const { data: activeMail } = useGetMailMessageQuery({ emailId: activeMailId! }, { skip: !activeMailId }); const [markAsRead] = useMarkAsReadMutation(); @@ -49,12 +49,12 @@ const MailView = ({ folder }: MailViewProps) => { {/* Tray */} {/* Mail Preview */}
diff --git a/src/hooks/mail/useListFolderPaginated.test.tsx b/src/hooks/mail/useListFolderPaginated.test.tsx index 92adc81..64cbe9f 100644 --- a/src/hooks/mail/useListFolderPaginated.test.tsx +++ b/src/hooks/mail/useListFolderPaginated.test.tsx @@ -31,8 +31,8 @@ describe('List Folder Paginated - custom hook', () => { waitFor(() => { expect(result.current.isLoadingListFolder).toBe(true); - expect(result.current.listFolder?.emails).toHaveLength(DEFAULT_FOLDER_LIMIT); - expect(result.current.hasMore).toBeTruthy(); + expect(result.current.listFolderEmails).toHaveLength(DEFAULT_FOLDER_LIMIT); + expect(result.current.hasMoreEmails).toBeTruthy(); }); }); @@ -46,7 +46,7 @@ describe('List Folder Paginated - custom hook', () => { wrapper: createWrapper(store), }); - expect(result.current.hasMore).toBeFalsy(); + expect(result.current.hasMoreEmails).toBeFalsy(); }); test('When the user scrolls to the end of the list, then the next batch of emails is loaded and appended', async () => { @@ -67,9 +67,9 @@ describe('List Folder Paginated - custom hook', () => { }); waitFor(() => { - expect(result.current.listFolder?.emails).toHaveLength(DEFAULT_FOLDER_LIMIT * 2); - expect(result.current.listFolder?.emails).toStrictEqual([...page1.emails, ...page2.emails]); - expect(result.current.hasMore).toBeFalsy(); + expect(result.current.listFolderEmails).toHaveLength(DEFAULT_FOLDER_LIMIT * 2); + expect(result.current.listFolderEmails).toStrictEqual([...page1.emails, ...page2.emails]); + expect(result.current.hasMoreEmails).toBeFalsy(); }); }); @@ -105,14 +105,14 @@ describe('List Folder Paginated - custom hook', () => { }); waitFor(() => { - expect(result.current.listFolder?.emails).toStrictEqual(inboxEmails.emails); + expect(result.current.listFolderEmails).toStrictEqual(inboxEmails.emails); }); rerender({ mailbox: 'sent' }); waitFor(() => { - expect(result.current.listFolder?.emails).toStrictEqual(sentEmails.emails); - expect(result.current.listFolder?.emails).toHaveLength(DEFAULT_FOLDER_LIMIT); + expect(result.current.listFolderEmails).toStrictEqual(sentEmails.emails); + expect(result.current.listFolderEmails).toHaveLength(DEFAULT_FOLDER_LIMIT); }); }); }); diff --git a/src/hooks/mail/useListFolderPaginated.ts b/src/hooks/mail/useListFolderPaginated.ts index a98111d..380d202 100644 --- a/src/hooks/mail/useListFolderPaginated.ts +++ b/src/hooks/mail/useListFolderPaginated.ts @@ -1,15 +1,10 @@ -/* eslint-disable react-hooks/set-state-in-effect */ import { DEFAULT_FOLDER_LIMIT } from '@/constants'; import { useGetListFolderQuery } from '@/store/api/mail'; import type { FolderType } from '@/types/mail'; -import { useEffect, useState } from 'react'; +import { useState } from 'react'; const useListFolderPaginated = (mailbox: FolderType) => { - const [position, setPosition] = useState(0); - - useEffect(() => { - setPosition(0); - }, [mailbox]); + const [anchorId, setAnchorId] = useState(undefined); const { data: listFolder, @@ -19,7 +14,7 @@ const useListFolderPaginated = (mailbox: FolderType) => { { mailbox, limit: DEFAULT_FOLDER_LIMIT, - position, + anchorId, }, { pollingInterval: 30000, @@ -27,18 +22,17 @@ const useListFolderPaginated = (mailbox: FolderType) => { }, ); - const hasMore = (listFolder?.emails.length ?? 0) < (listFolder?.total ?? 0); - const onLoadMore = () => { - if (isFetching || !hasMore) return; - setPosition((prev) => prev + DEFAULT_FOLDER_LIMIT); + if (isFetching || !listFolder?.hasMoreMails) return; + + setAnchorId(listFolder?.nextAnchor); }; return { - listFolder, + listFolderEmails: listFolder?.emails, isLoadingListFolder, onLoadMore, - hasMore, + hasMoreEmails: listFolder?.hasMoreMails, }; }; diff --git a/src/services/sdk/index.ts b/src/services/sdk/index.ts index 4187d7b..5a4fe5e 100644 --- a/src/services/sdk/index.ts +++ b/src/services/sdk/index.ts @@ -3,9 +3,8 @@ import type { ApiSecurity, AppDetails } from '@internxt/sdk/dist/shared'; import packageJson from '../../../package.json'; import { ConfigService } from '../config'; import { LocalStorageService } from '../local-storage'; -import { AuthService } from './auth'; -import { NavigationService } from '../navigation'; -import { AppView } from '@/routes/paths'; +import { store } from '@/store'; +import { logoutThunk } from '@/store/slices/user/thunks'; export type SdkManagerApiSecurity = ApiSecurity & { newToken: string }; @@ -33,14 +32,8 @@ export class SdkManager { private getNewTokenApiSecurity(): ApiSecurity { return { token: LocalStorageService.instance?.getToken() ?? '', - unauthorizedCallback: () => { - if (LocalStorageService.instance) { - LocalStorageService.instance.clearCredentials(); - AuthService.instance.logOut().catch((error) => { - console.error(error); - }); - NavigationService.instance.navigate({ id: AppView.Welcome }); - } + unauthorizedCallback: async () => { + await store.dispatch(logoutThunk()); }, }; } diff --git a/src/services/sdk/sdk.service.test.ts b/src/services/sdk/sdk.service.test.ts index 1e31ee8..4a5b407 100644 --- a/src/services/sdk/sdk.service.test.ts +++ b/src/services/sdk/sdk.service.test.ts @@ -3,8 +3,14 @@ import { beforeEach, describe, expect, test, vi, afterEach } from 'vitest'; import { SdkManager } from '.'; import { ConfigService } from '../config'; import { LocalStorageService } from '../local-storage'; -import { AuthService } from './auth'; import { NavigationService } from '../navigation'; +import { store } from '@/store'; + +vi.mock('@/store', () => ({ + store: { + dispatch: vi.fn(), + }, +})); vi.mock('./auth', () => ({ AuthService: { @@ -202,7 +208,7 @@ describe('SDK Manager', () => { const securityArg = (Drive.Users.client as any).mock.calls[0][2]; securityArg.unauthorizedCallback(); - expect(AuthService.instance.logOut).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalled(); }); }); @@ -232,7 +238,7 @@ describe('SDK Manager', () => { const securityArg = (Drive.Storage.client as any).mock.calls[0][2]; securityArg.unauthorizedCallback(); - expect(AuthService.instance.logOut).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalled(); }); }); @@ -262,7 +268,7 @@ describe('SDK Manager', () => { const securityArg = (Drive.Payments.client as any).mock.calls[0][2]; securityArg.unauthorizedCallback(); - expect(AuthService.instance.logOut).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalled(); }); }); @@ -292,7 +298,7 @@ describe('SDK Manager', () => { const securityArg = (Mail.client as any).mock.calls[0][2]; securityArg.unauthorizedCallback(); - expect(AuthService.instance.logOut).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalled(); }); }); }); diff --git a/src/store/api/mail/index.ts b/src/store/api/mail/index.ts index e5cd9b8..44f01a6 100644 --- a/src/store/api/mail/index.ts +++ b/src/store/api/mail/index.ts @@ -22,18 +22,17 @@ export const mailApi = api.injectEndpoints({ getListFolder: builder.query({ serializeQueryArgs: ({ queryArgs }) => ({ mailbox: queryArgs?.mailbox }), merge: (currentCache, newItems, { arg }) => { - const currentPosition = arg?.position ?? 0; - - // This prevents the concatenation of the existent cached emails with the new ones (repeated emails) - if (currentPosition === 0) { - currentCache.emails = newItems.emails; - } else { + // No anchorId means first page — replace instead of accumulate + if (arg?.anchorId) { currentCache.emails.push(...newItems.emails); + } else { + currentCache.emails = newItems.emails; } - currentCache.total = newItems.total; + currentCache.hasMoreMails = newItems.hasMoreMails; + currentCache.nextAnchor = newItems.nextAnchor; }, forceRefetch: ({ currentArg, previousArg }) => - currentArg?.mailbox !== previousArg?.mailbox || currentArg?.position !== previousArg?.position, + currentArg?.mailbox !== previousArg?.mailbox || currentArg?.anchorId !== previousArg?.anchorId, async queryFn(query) { try { const mailList = await MailService.instance.listFolder(query); diff --git a/src/store/api/mail/mail.api.test.ts b/src/store/api/mail/mail.api.test.ts index 11ceafa..688b575 100644 --- a/src/store/api/mail/mail.api.test.ts +++ b/src/store/api/mail/mail.api.test.ts @@ -85,7 +85,7 @@ describe('Mail Query', () => { const store = createTestStore(); await store.dispatch(mailApi.endpoints.getListFolder.initiate(query as ListEmailsQuery)); await store.dispatch( - mailApi.endpoints.getListFolder.initiate({ ...query, position: DEFAULT_FOLDER_LIMIT } as ListEmailsQuery), + mailApi.endpoints.getListFolder.initiate({ ...query, anchorId: 'anchor-page-2' } as ListEmailsQuery), ); const state = store.getState() as unknown as RootState; @@ -111,7 +111,7 @@ describe('Mail Query', () => { const cache = mailApi.endpoints.getListFolder.select(query as ListEmailsQuery)(state); expect(cache.data?.emails).toHaveLength(DEFAULT_FOLDER_LIMIT); - expect(cache.data?.emails).toEqual(reload.emails); + expect(cache.data?.emails).toStrictEqual(reload.emails); }); }); @@ -141,7 +141,7 @@ describe('Mail Query', () => { }); describe('Mark Mail As Read', () => { - const mailboxQuery = { mailbox: 'inbox', limit: DEFAULT_FOLDER_LIMIT, position: 0 } as ListEmailsQuery; + const mailboxQuery = { mailbox: 'inbox', limit: DEFAULT_FOLDER_LIMIT } as ListEmailsQuery; const setupOptimisticStore = async () => { const mockedMails = getMockedMails(); diff --git a/vite.config.ts b/vite.config.ts index 08921b3..4233901 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -27,6 +27,7 @@ export default defineConfig({ alias: { '@': path.resolve(__dirname, './src'), }, + preserveSymlinks: true, }, optimizeDeps: { include: ['@internxt/sdk'],