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>({
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;
43 organization: data.organization
46 /** If the response has a `since` property, it is paginated:
47 * Append to the current organization emails list */
49 ...(data.since ? (prev.organization?.emails ?? []) : []),
50 ...data.organization.emails,
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);
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 }));
75 state: { ...state, loading },
77 if (!loading && state.more && state.next) {
78 recommendations.revalidate({ pageSize, shareId, since: state.next, startsWith }, requestId);