Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / hooks / useInviteRecommendations.ts
blob51138e0e41919d520fe30d5b5f0cb1ba7d8eab9d
1 import { useEffect, useMemo, useRef, useState } from 'react';
2 import { useDispatch } from 'react-redux';
4 import { useActionRequest } from '@proton/pass/hooks/useActionRequest';
5 import { useDebouncedValue } from '@proton/pass/hooks/useDebouncedValue';
6 import type { inviteRecommendationsFailure, inviteRecommendationsSuccess } from '@proton/pass/store/actions';
7 import { getShareAccessOptions, inviteRecommendationsIntent } from '@proton/pass/store/actions';
8 import type { MaybeNull } from '@proton/pass/types';
9 import type { InviteRecommendationsSuccess } from '@proton/pass/types/data/invites.dto';
10 import { uniqueId } from '@proton/pass/utils/string/unique-id';
12 type Options = { shareId: string; pageSize: number };
14 export const useInviteRecommendations = (autocomplete: string, { shareId, pageSize }: Options) => {
15     const dispatch = useDispatch();
16     /** Keep a unique requestId per mount to allow multiple components
17      * to independently request recommendation data */
18     const requestId = useMemo(() => uniqueId(), []);
19     const startsWith = useDebouncedValue(autocomplete, 250);
20     const emptyBoundary = useRef<MaybeNull<string>>(null);
21     const didLoad = useRef(false);
23     const [state, setState] = useState<InviteRecommendationsSuccess>({
24         emails: [],
25         organization: null,
26         next: null,
27         more: false,
28         since: null,
29     });
31     const { loading, ...recommendations } = useActionRequest<
32         typeof inviteRecommendationsIntent,
33         typeof inviteRecommendationsSuccess,
34         typeof inviteRecommendationsFailure
35     >(inviteRecommendationsIntent, {
36         onSuccess: ({ data }) => {
37             didLoad.current = true;
38             const empty = data.emails.length + (data.organization?.emails.length ?? 0) === 0;
39             emptyBoundary.current = empty ? startsWith : null;
41             setState((prev) => ({
42                 ...data,
43                 organization: data.organization
44                     ? {
45                           ...data.organization,
46                           /** If the response has a `since` property, it is paginated:
47                            * Append to the current organization emails list */
48                           emails: [
49                               ...(data.since ? (prev.organization?.emails ?? []) : []),
50                               ...data.organization.emails,
51                           ],
52                       }
53                     : null,
54             }));
55         },
56     });
58     useEffect(() => {
59         /** Trigger initial recommendation request when component mounts.
60          * Force a revalidation of the share's access options to have fresh
61          * member data when reconciliating suggestions against current members */
62         dispatch(getShareAccessOptions.intent({ shareId }));
63         recommendations.dispatch({ pageSize, shareId, since: null, startsWith: '' }, requestId);
64     }, []);
66     useEffect(() => {
67         if (didLoad.current) {
68             if (emptyBoundary.current && startsWith.startsWith(emptyBoundary.current)) return;
69             recommendations.revalidate({ pageSize, shareId, since: null, startsWith }, requestId);
70             setState((prev) => ({ ...prev, since: null }));
71         }
72     }, [startsWith]);
74     return {
75         state: { ...state, loading },
76         loadMore: () => {
77             if (!loading && state.more && state.next) {
78                 recommendations.revalidate({ pageSize, shareId, since: state.next, startsWith }, requestId);
79             }
80         },
81     };