Merge branch 'feat/inda-383-daily-stat' into 'main'
[ProtonMail-WebClient.git] / packages / mail / contacts / index.ts
blobe838b07ac58bea8ca12ad7fd4ae058ea32f6da87
1 import { createSlice } from '@reduxjs/toolkit';
3 import { type ModelState, getInitialModelState, serverEvent } from '@proton/account';
4 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
5 import { createAsyncModelThunk, handleAsyncModel, previousSelector } from '@proton/redux-utilities';
6 import { queryContacts } from '@proton/shared/lib/api/contacts';
7 import queryPages from '@proton/shared/lib/api/helpers/queryPages';
8 import { CONTACTS_LIMIT, CONTACTS_REQUESTS_PER_SECOND } from '@proton/shared/lib/constants';
9 import { EVENT_ERRORS } from '@proton/shared/lib/errors';
10 import { hasBit } from '@proton/shared/lib/helpers/bitset';
11 import updateCollection from '@proton/shared/lib/helpers/updateCollection';
12 import { type Api } from '@proton/shared/lib/interfaces';
13 import { type Contact } from '@proton/shared/lib/interfaces/contacts';
15 const name = 'contacts' as const;
17 const compareName = (a: Contact, b: Contact) => a.Name.localeCompare(b.Name);
19 export const getContactsModel = (api: Api) => {
20     return queryPages(
21         (Page, PageSize) =>
22             api(
23                 queryContacts({
24                     Page,
25                     PageSize,
26                 })
27             ),
28         {
29             pageSize: CONTACTS_LIMIT,
30             pagesPerChunk: CONTACTS_REQUESTS_PER_SECOND,
31         }
32     ).then((pages) => {
33         return pages.flatMap(({ Contacts }) => Contacts).sort(compareName);
34     });
37 interface State {
38     [name]: ModelState<Contact[]>;
41 type SliceState = State[typeof name];
42 type Model = NonNullable<SliceState['value']>;
44 export const selectContacts = (state: State) => state[name];
46 const modelThunk = createAsyncModelThunk<Model, State, ProtonThunkArguments>(`${name}/fetch`, {
47     miss: async ({ extraArgument }) => {
48         return getContactsModel(extraArgument.api);
49     },
50     previous: previousSelector(selectContacts),
51 });
53 const initialState = getInitialModelState<Model>();
54 const slice = createSlice({
55     name,
56     initialState,
57     reducers: {},
58     extraReducers: (builder) => {
59         handleAsyncModel(builder, modelThunk);
60         builder.addCase(serverEvent, (state, action) => {
61             if (state.value && action.payload.Contacts) {
62                 state.value = updateCollection({
63                     model: state.value,
64                     events: action.payload.Contacts,
65                     itemKey: 'Contact',
66                     // Event updates return the full item to use
67                     merge: (_, b) => b as Contact,
68                 }).sort(compareName);
69             }
70             if (state.value && hasBit(action.payload.Refresh, EVENT_ERRORS.CONTACTS)) {
71                 delete state.value;
72                 delete state.error;
73             }
74         });
75     },
76 });
78 export const contactsReducer = { [name]: slice.reducer };
79 export const contactsThunk = modelThunk.thunk;