Update selected item color in Pass menu
[ProtonMail-WebClient.git] / packages / pass / store / sagas / events / channel.shares.ts
blob7d5ddd37513b751032f367b23504fc190d7180b6
1 /* eslint-disable @typescript-eslint/no-throw-literal, curly */
2 import { all, fork, put, select, takeEvery } from 'redux-saga/effects';
4 import { type EventManagerEvent, NOOP_EVENT } from '@proton/pass/lib/events/manager';
5 import { requestItemsForShareId } from '@proton/pass/lib/items/item.requests';
6 import { parseShareResponse } from '@proton/pass/lib/shares/share.parser';
7 import { hasShareAccessChanged } from '@proton/pass/lib/shares/share.predicates';
8 import { shareAccessChange, sharedVaultCreated, sharesSync, vaultCreationSuccess } from '@proton/pass/store/actions';
9 import type { ItemsByShareId } from '@proton/pass/store/reducers';
10 import { selectAllShares } from '@proton/pass/store/selectors';
11 import type { RootSagaOptions } from '@proton/pass/store/types';
12 import type { Api, Maybe, Share, ShareRole, SharesGetResponse } from '@proton/pass/types';
13 import { ShareType } from '@proton/pass/types';
14 import { or, truthy } from '@proton/pass/utils/fp/predicates';
15 import { diadic } from '@proton/pass/utils/fp/variadics';
16 import { logger } from '@proton/pass/utils/logger';
17 import { merge } from '@proton/pass/utils/object/merge';
18 import { toMap } from '@proton/shared/lib/helpers/object';
20 import { eventChannelFactory } from './channel.factory';
21 import { getShareChannelForks } from './channel.share';
22 import { channelEventsWorker, channelInitWorker } from './channel.worker';
23 import type { EventChannel } from './types';
25 /** We're only interested in new shares in this effect : Deleted shares will
26  * be handled by the share's EventChannel error handling. see `channel.share.ts`
27  * code `PassErrorCode.DISABLED_SHARE`. FIXME: handle ItemShares */
28 function* onSharesEvent(
29     event: EventManagerEvent<SharesGetResponse>,
30     { api }: EventChannel<any>,
31     options: RootSagaOptions
32 ) {
33     if ('error' in event) throw event.error;
35     const localShares: Share[] = yield select(selectAllShares);
36     const localShareIds = localShares.map(({ shareId }) => shareId);
38     const remoteShares = event.Shares;
40     const newShares = remoteShares.filter((share) => !localShareIds.includes(share.ShareID));
42     if (newShares.length) {
43         logger.info(`[ServerEvents::Shares]`, `${newShares.length} remote share(s) not in cache`);
45         const activeNewShares = (
46             (yield Promise.all(
47                 newShares
48                     .filter((share) => share.TargetType === ShareType.Vault)
49                     .map((encryptedShare) => parseShareResponse(encryptedShare))
50             )) as Maybe<Share>[]
51         ).filter(truthy);
53         if (activeNewShares.length > 0) {
54             const items = (
55                 (yield Promise.all(
56                     activeNewShares.map(async ({ shareId }) => ({
57                         [shareId]: toMap(await requestItemsForShareId(shareId), 'itemId'),
58                     }))
59                 )) as ItemsByShareId[]
60             ).reduce(diadic(merge));
62             yield put(sharesSync({ shares: toMap(activeNewShares, 'shareId'), items }));
63             yield all(activeNewShares.map(getShareChannelForks(api, options)).flat());
64         }
65     }
67     yield fork(function* () {
68         for (const share of remoteShares) {
69             const shareId = share.ShareID;
70             const localShare = localShares.find((localShare) => localShare.shareId === shareId);
72             if (localShare && hasShareAccessChanged(localShare, share)) {
73                 yield put(
74                     shareAccessChange({
75                         newUserInvitesReady: share.NewUserInvitesReady,
76                         owner: share.Owner,
77                         shared: share.Shared,
78                         shareId,
79                         shareRoleId: share.ShareRoleID as ShareRole,
80                         targetMaxMembers: share.TargetMaxMembers,
81                         targetMembers: share.TargetMembers,
82                     })
83                 );
84             }
85         }
86     });
89 /* The event-manager can be used to implement
90  * a polling mechanism if we conform to the data
91  * structure it is expecting. In order to poll for
92  * new shares, set the query accordingly & use a
93  * non-existing eventID */
94 export const createSharesChannel = (api: Api) =>
95     eventChannelFactory<SharesGetResponse>({
96         api,
97         channelId: 'shares',
98         initialEventID: NOOP_EVENT,
99         getCursor: () => ({ EventID: NOOP_EVENT, More: false }),
100         onClose: () => logger.info(`[ServerEvents::Shares] closing channel`),
101         onEvent: onSharesEvent,
102         query: () => ({ url: 'pass/v1/share', method: 'get' }),
103     });
105 /* when a vault is created : recreate all the necessary
106  * channels to start polling for this new share's events */
107 function* onNewShare(api: Api, options: RootSagaOptions) {
108     yield takeEvery(or(vaultCreationSuccess.match, sharedVaultCreated.match), function* ({ payload: { share } }) {
109         yield all(getShareChannelForks(api, options)(share));
110     });
113 export function* sharesChannel(api: Api, options: RootSagaOptions) {
114     logger.info(`[ServerEvents::Shares] start polling for new shares`);
115     const eventsChannel = createSharesChannel(api);
116     const events = fork(channelEventsWorker<SharesGetResponse>, eventsChannel, options);
117     const wakeup = fork(channelInitWorker<SharesGetResponse>, eventsChannel, options);
118     const newVault = fork(onNewShare, api, options);
120     yield all([events, wakeup, newVault]);