1 import { c } from 'ttag';
3 import { MAX_BATCH_PER_REQUEST } from '@proton/pass/constants';
4 import PassCoreUI from '@proton/pass/lib/core/core.ui';
5 import type { Draft } from '@proton/pass/store/reducers';
19 } from '@proton/pass/types';
20 import { groupByKey } from '@proton/pass/utils/array/group-by-key';
21 import { arrayInterpolate } from '@proton/pass/utils/array/interpolate';
22 import { deobfuscate } from '@proton/pass/utils/obfuscate/xor';
23 import { UNIX_DAY, UNIX_MONTH, UNIX_WEEK } from '@proton/pass/utils/time/constants';
24 import { getEpoch } from '@proton/pass/utils/time/epoch';
25 import chunk from '@proton/utils/chunk';
27 import { hasUserIdentifier, isEditItemDraft } from './item.predicates';
29 const SEPERATOR = '::';
30 const toKey = (...args: (string | number)[]) => args.join(SEPERATOR);
32 export const getItemKeyRevision = ({ shareId, itemId, revision }: ItemRevision) => toKey(shareId, itemId, revision);
34 export const getItemKey = <T extends UniqueItem>({ shareId, itemId }: T) => toKey(shareId, itemId);
36 export const fromItemKey = (key: string): UniqueItem => {
37 const [shareId, itemId] = key.split(SEPERATOR);
38 return { itemId, shareId };
41 export const intoSelectedItem = ({ shareId, itemId }: ItemRevision): SelectedItem => ({ shareId, itemId });
43 export const getItemActionId = (
45 | { optimisticId: string; itemId?: string; shareId: string }
46 | { optimisticId?: string; itemId: string; shareId: string }
47 ) => toKey(payload.shareId, payload?.optimisticId ?? payload.itemId!);
49 export const flattenItemsByShareId = (itemsByShareId: {
50 [shareId: string]: { [itemId: string]: ItemRevision };
51 }): ItemRevision[] => Object.values(itemsByShareId).flatMap(Object.values);
53 export const interpolateRecentItems =
54 <T extends ItemRevision>(items: T[]) =>
55 (shouldInterpolate: boolean) => {
56 type DateCluster = { label: string; boundary: number };
57 const now = getEpoch();
59 return arrayInterpolate<T, DateCluster>(items, {
60 clusters: shouldInterpolate
63 // translator: label means items that have been added or edited in last 24 hours from the current moment
64 label: c('Label').t`Today`,
65 boundary: now - UNIX_DAY,
68 // translator: label means items that have been added or edited in last 7 days from the current moment
69 label: c('Label').t`Last week`,
70 boundary: now - UNIX_WEEK,
73 // translator: label means items that have been added or edited in last 14 days from the current moment
74 label: c('Label').t`Last 2 weeks`,
75 boundary: now - UNIX_WEEK * 2,
78 // translator: label means items that have been added or edited in last 4 weeks from the current moment
79 label: c('Label').t`Last month`,
80 boundary: now - UNIX_MONTH,
85 // translator: label means items that have been added or edited more than a month ago (4 weeks) from the current moment
86 label: c('Label').t`More than a month`,
89 shouldInterpolate: ({ lastUseTime, modifyTime }, { boundary }) =>
90 Math.max(lastUseTime ?? modifyTime, modifyTime) > boundary,
94 export const filterItemsByShareId =
95 (shareId?: MaybeNull<string>) =>
96 <T extends ItemRevision>(items: T[]) => {
97 if (!shareId) return items;
98 return items.filter((item) => shareId === item.shareId);
101 export const filterItemsByType =
102 (itemType?: MaybeNull<ItemType>) =>
103 <T extends ItemRevision>(items: T[]) => {
104 if (!itemType) return items;
105 return items.filter((item) => !itemType || itemType === item.data.type);
108 export const filterItemsByUserIdentifier = (email: string) => (items: LoginItem[]) =>
109 items.reduce<LoginItem[]>((acc, item) => {
110 if (hasUserIdentifier(email)(item)) acc.push(item);
114 export const sortItems =
115 (sort?: MaybeNull<ItemSortFilter>) =>
116 <T extends ItemRevision>(items: T[]) => {
117 if (!sort) return items;
119 return items.slice().sort((a, b) => {
121 case 'createTimeASC':
122 return a.createTime - b.createTime;
123 case 'createTimeDESC':
124 return b.createTime - a.createTime;
127 Math.max(b.lastUseTime ?? b.modifyTime, b.modifyTime) -
128 Math.max(a.lastUseTime ?? a.modifyTime, a.modifyTime)
131 return a.data.metadata.name.localeCompare(b.data.metadata.name);
136 /** Filters the drafts for a given a shareId. If itemIds are provided it will
137 * also try to match for these specifics items */
138 export const matchDraftsForShare = (drafts: Draft[], shareId: string, itemIds?: string[]) =>
139 drafts.filter((draft) => {
140 if (isEditItemDraft(draft) && draft.shareId === shareId) {
141 return itemIds === undefined || itemIds.includes(draft.itemId);
147 /** Batches a list of items by shareId : each individual share batch
148 * is in turn batched according to `MAX_BATCH_ITEMS_PER_REQUEST` */
149 export const batchByShareId = <T extends UniqueItem, R>(
151 mapTo: (item: T) => R
152 ): { shareId: string; items: R[] }[] =>
153 groupByKey(items, 'shareId').flatMap((shareTrashedItems) => {
154 const batches = chunk(shareTrashedItems, MAX_BATCH_PER_REQUEST);
155 return batches.map((batch) => ({
156 shareId: batch[0].shareId,
157 items: batch.map(mapTo),
161 /** Converts an item revision to a revision request payload */
162 export const intoRevisionID = (item: ItemRevision): ItemRevisionID => ({
164 Revision: item.revision,
167 export const intoUserIdentifier = (item: ItemRevision<'login'>): string =>
168 /** For autofill we use the username if not empty, otherwise the email */
169 deobfuscate(item.data.content.itemUsername) || deobfuscate(item.data.content.itemEmail);
171 export const intoLoginItemPreview = (item: ItemRevision<'login'>): LoginItemPreview => ({
173 shareId: item.shareId,
174 name: item.data.metadata.name,
175 userIdentifier: intoUserIdentifier(item),
176 url: item.data.content.urls?.[0],
179 export const intoIdentityItemPreview = (item: ItemRevision<'identity'>): IdentityItemPreview => ({
181 shareId: item.shareId,
182 name: item.data.metadata.name,
183 fullName: item.data.content.fullName,
186 export const getSanitizedUserIdentifiers = ({
189 }: Pick<UnsafeItem<'login'>['content'], 'itemEmail' | 'itemUsername'>) => {
190 const validEmail = PassCoreUI.is_email_valid(itemEmail);
191 const emailUsername = PassCoreUI.is_email_valid(itemUsername);
194 /* `itemEmail` is empty and `itemUsername` is a valid email: Move username to email field */
195 if (!itemEmail && emailUsername) return { email: itemUsername, username: '' };
196 /* `itemEmail` is invalid but `itemUsername` is a valid email: Swap email and username */
197 if (!validEmail && emailUsername) return { email: itemUsername, username: itemEmail };
198 /* All other cases, return in-place */
199 return { email: itemEmail, username: itemUsername };
202 /* If `itemEmail` is valid, keep it; otherwise, move it to username field */
203 return validEmail ? { email: itemEmail, username: '' } : { email: '', username: itemEmail };
206 export const intoBulkSelection = (items: UniqueItem[]): BulkSelectionDTO =>
207 items.reduce<BulkSelectionDTO>((dto, { shareId, itemId }) => {
208 dto[shareId] = dto[shareId] ?? {};
209 dto[shareId][itemId] = true;
213 export const getBulkSelectionCount = (selected: BulkSelectionDTO) =>
214 Object.values(selected).reduce((acc, items) => acc + Object.keys(items).length, 0);
216 export const formatDisplayNameWithEmail = (name: string, email: string) => `${name} <${email}>`;