ente/src/services/collectionService.ts

729 lines
21 KiB
TypeScript
Raw Normal View History

2021-05-30 16:56:48 +00:00
import { getEndpoint } from 'utils/common/apiUtil';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
2021-03-12 06:58:27 +00:00
import localForage from 'utils/storage/localForage';
2021-05-30 16:56:48 +00:00
import { getActualKey, getToken } from 'utils/common/key';
2021-04-03 04:36:15 +00:00
import CryptoWorker from 'utils/crypto';
2021-05-30 16:56:48 +00:00
import { SetDialogMessage } from 'components/MessageDialog';
import constants from 'utils/strings/constants';
import { getPublicKey } from './userService';
2021-08-13 03:19:48 +00:00
import { B64EncryptionResult } from 'utils/crypto';
2021-05-29 06:27:52 +00:00
import HTTPService from './HTTPService';
2022-01-04 09:50:14 +00:00
import { EnteFile } from 'types/file';
2021-06-12 17:14:21 +00:00
import { logError } from 'utils/sentry';
import { CustomError } from 'utils/error';
2021-09-28 13:51:54 +00:00
import { sortFiles } from 'utils/file';
import {
Collection,
CollectionAndItsLatestFile,
AddToCollectionRequest,
MoveToCollectionRequest,
EncryptedFileKey,
RemoveFromCollectionRequest,
CreatePublicAccessTokenRequest,
2022-01-24 11:03:33 +00:00
PublicURL,
} from 'types/collection';
import {
COLLECTION_SORT_BY,
CollectionType,
COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT,
COLLECTION_SHARE_DEFAULT_VALID_DURATION,
} from 'constants/collection';
const ENDPOINT = getEndpoint();
const COLLECTION_TABLE = 'collections';
const COLLECTION_UPDATION_TIME = 'collection-updation-time';
const getCollectionWithSecrets = async (
collection: Collection,
2021-08-13 02:38:38 +00:00
masterKey: string
) => {
const worker = await new CryptoWorker();
const userID = getData(LS_KEYS.USER).id;
let decryptedKey: string;
2021-05-29 06:27:52 +00:00
if (collection.owner.id === userID) {
decryptedKey = await worker.decryptB64(
collection.encryptedKey,
collection.keyDecryptionNonce,
2021-08-13 02:38:38 +00:00
masterKey
);
} else {
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await worker.decryptB64(
keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce,
2021-08-13 02:38:38 +00:00
masterKey
);
decryptedKey = await worker.boxSealOpen(
collection.encryptedKey,
keyAttributes.publicKey,
2021-08-13 02:38:38 +00:00
secretKey
);
}
2021-08-13 02:38:38 +00:00
collection.name =
collection.name ||
2021-02-16 11:43:21 +00:00
(await worker.decryptToUTF8(
collection.encryptedName,
collection.nameDecryptionNonce,
2021-08-13 02:38:38 +00:00
decryptedKey
));
return {
...collection,
key: decryptedKey,
};
};
const getCollections = async (
token: string,
sinceTime: number,
2021-08-13 02:38:38 +00:00
key: string
): Promise<Collection[]> => {
2021-01-25 07:05:45 +00:00
try {
const resp = await HTTPService.get(
`${ENDPOINT}/collections`,
{
2021-05-29 06:27:52 +00:00
sinceTime,
},
2021-08-13 02:38:38 +00:00
{ 'X-Auth-Token': token }
);
const promises: Promise<Collection>[] = resp.data.collections.map(
async (collection: Collection) => {
if (collection.isDeleted) {
return collection;
}
let collectionWithSecrets = collection;
try {
collectionWithSecrets = await getCollectionWithSecrets(
collection,
2021-08-13 02:38:38 +00:00
key
);
} catch (e) {
logError(e, `decryption failed for collection`, {
collectionID: collection.id,
});
}
return collectionWithSecrets;
2021-08-13 02:38:38 +00:00
}
);
// only allow deleted or collection with key, filtering out collection whose decryption failed
const collections = (await Promise.all(promises)).filter(
(collection) => collection.isDeleted || collection.key
);
return collections;
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'getCollections failed');
2021-05-24 13:11:08 +00:00
throw e;
2021-01-25 07:05:45 +00:00
}
};
export const getLocalCollections = async (): Promise<Collection[]> => {
2021-08-13 02:38:38 +00:00
const collections: Collection[] =
(await localForage.getItem(COLLECTION_TABLE)) ?? [];
return collections;
};
2021-08-13 02:38:38 +00:00
export const getCollectionUpdationTime = async (): Promise<number> =>
(await localForage.getItem<number>(COLLECTION_UPDATION_TIME)) ?? 0;
export const syncCollections = async () => {
const localCollections = await getLocalCollections();
const lastCollectionUpdationTime = await getCollectionUpdationTime();
2021-04-29 08:58:34 +00:00
const token = getToken();
2021-05-29 06:27:52 +00:00
const key = await getActualKey();
2021-08-13 02:38:38 +00:00
const updatedCollections =
(await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
2021-05-29 06:27:52 +00:00
if (updatedCollections.length === 0) {
return localCollections;
}
const allCollectionsInstances = [
...localCollections,
...updatedCollections,
];
2021-05-29 06:27:52 +00:00
const latestCollectionsInstances = new Map<number, Collection>();
allCollectionsInstances.forEach((collection) => {
if (
!latestCollectionsInstances.has(collection.id) ||
latestCollectionsInstances.get(collection.id).updationTime <
2021-08-13 02:38:38 +00:00
collection.updationTime
) {
latestCollectionsInstances.set(collection.id, collection);
}
});
2021-09-30 07:32:58 +00:00
let collections: Collection[] = [];
2021-05-29 06:27:52 +00:00
let updationTime = await localForage.getItem<number>(
2021-08-13 02:38:38 +00:00
COLLECTION_UPDATION_TIME
2021-05-29 06:27:52 +00:00
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_, collection] of latestCollectionsInstances) {
if (!collection.isDeleted) {
2021-02-06 07:51:49 +00:00
collections.push(collection);
updationTime = Math.max(updationTime, collection.updationTime);
2021-02-08 07:33:46 +00:00
}
}
2021-09-30 07:32:58 +00:00
collections = sortCollections(
collections,
[],
COLLECTION_SORT_BY.MODIFICATION_TIME
);
await localForage.setItem(COLLECTION_TABLE, collections);
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
return collections;
};
2021-01-13 12:43:46 +00:00
2021-10-01 06:45:44 +00:00
export const getCollection = async (
collectionID: number
): Promise<Collection> => {
try {
const token = getToken();
if (!token) {
return;
}
const resp = await HTTPService.get(
`${ENDPOINT}/collections/${collectionID}`,
null,
{ 'X-Auth-Token': token }
);
2021-10-04 08:15:32 +00:00
const key = await getActualKey();
const collectionWithSecrets = await getCollectionWithSecrets(
resp.data?.collection,
key
);
return collectionWithSecrets;
2021-10-01 06:45:44 +00:00
} catch (e) {
logError(e, 'failed to get collection', { collectionID });
}
2021-09-10 03:41:38 +00:00
};
export const getCollectionsAndTheirLatestFile = (
collections: Collection[],
2022-01-04 09:50:14 +00:00
files: EnteFile[]
): CollectionAndItsLatestFile[] => {
2022-01-04 09:50:14 +00:00
const latestFile = new Map<number, EnteFile>();
files.forEach((file) => {
if (!latestFile.has(file.collectionID)) {
latestFile.set(file.collectionID, file);
}
});
2021-05-29 06:27:52 +00:00
const collectionsAndTheirLatestFile: CollectionAndItsLatestFile[] = [];
for (const collection of collections) {
collectionsAndTheirLatestFile.push({
collection,
file: latestFile.get(collection.id),
});
}
return collectionsAndTheirLatestFile;
};
2022-01-04 09:50:14 +00:00
export const getFavItemIds = async (
files: EnteFile[]
): Promise<Set<number>> => {
2021-05-29 06:27:52 +00:00
const favCollection = await getFavCollection();
if (!favCollection) return new Set();
2021-01-20 12:05:04 +00:00
return new Set(
files
2021-02-09 05:07:46 +00:00
.filter((file) => file.collectionID === favCollection.id)
2021-08-13 02:38:38 +00:00
.map((file): number => file.id)
);
};
2021-01-20 12:05:04 +00:00
export const createAlbum = async (
albumName: string,
existingCollection?: Collection[]
) => createCollection(albumName, CollectionType.album, existingCollection);
2021-01-15 11:11:06 +00:00
export const createCollection = async (
collectionName: string,
type: CollectionType,
existingCollections?: Collection[]
): Promise<Collection> => {
try {
if (!existingCollections) {
existingCollections = await syncCollections();
}
2021-05-29 06:27:52 +00:00
for (const collection of existingCollections) {
if (collection.name === collectionName) {
return collection;
}
}
const worker = await new CryptoWorker();
const encryptionKey = await getActualKey();
const token = getToken();
const collectionKey: string = await worker.generateEncryptionKey();
const {
encryptedData: encryptedKey,
nonce: keyDecryptionNonce,
}: B64EncryptionResult = await worker.encryptToB64(
collectionKey,
2021-08-13 02:38:38 +00:00
encryptionKey
);
const {
encryptedData: encryptedName,
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
collectionName,
2021-08-13 02:38:38 +00:00
collectionKey
);
const newCollection: Collection = {
id: null,
owner: null,
encryptedKey,
keyDecryptionNonce,
encryptedName,
nameDecryptionNonce,
type,
attributes: {},
sharees: null,
updationTime: null,
isDeleted: false,
};
let createdCollection: Collection = await postCollection(
newCollection,
2021-08-13 02:38:38 +00:00
token
);
createdCollection = await getCollectionWithSecrets(
createdCollection,
2021-08-13 02:38:38 +00:00
encryptionKey
);
return createdCollection;
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'create collection failed');
2021-05-07 03:31:42 +00:00
throw e;
}
};
2021-01-13 12:43:46 +00:00
const postCollection = async (
collectionData: Collection,
2021-08-13 02:38:38 +00:00
token: string
): Promise<Collection> => {
2021-01-25 07:05:45 +00:00
try {
const response = await HTTPService.post(
`${ENDPOINT}/collections`,
collectionData,
null,
2021-08-13 02:38:38 +00:00
{ 'X-Auth-Token': token }
);
2021-01-25 07:05:45 +00:00
return response.data.collection;
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'post Collection failed ');
2021-01-25 07:05:45 +00:00
}
};
2021-01-15 11:11:06 +00:00
2022-01-04 09:50:14 +00:00
export const addToFavorites = async (file: EnteFile) => {
2021-09-10 04:04:38 +00:00
try {
let favCollection = await getFavCollection();
if (!favCollection) {
favCollection = await createCollection(
'Favorites',
CollectionType.favorites
);
const localCollections = await getLocalCollections();
await localForage.setItem(COLLECTION_TABLE, [
...localCollections,
favCollection,
]);
2021-09-10 04:04:38 +00:00
}
await addToCollection(favCollection, [file]);
} catch (e) {
logError(e, 'failed to add to favorite');
2021-01-15 11:11:06 +00:00
}
};
2021-01-15 11:11:06 +00:00
2022-01-04 09:50:14 +00:00
export const removeFromFavorites = async (file: EnteFile) => {
2021-09-10 04:04:38 +00:00
try {
const favCollection = await getFavCollection();
if (!favCollection) {
throw Error(CustomError.FAV_COLLECTION_MISSING);
}
await removeFromCollection(favCollection, [file]);
} catch (e) {
logError(e, 'remove from favorite failed');
}
};
2021-01-20 13:04:27 +00:00
2021-04-27 11:59:10 +00:00
export const addToCollection = async (
collection: Collection,
2022-01-04 09:50:14 +00:00
files: EnteFile[]
2021-04-27 11:59:10 +00:00
) => {
2021-01-25 07:05:45 +00:00
try {
const token = getToken();
2021-09-20 06:21:31 +00:00
const fileKeysEncryptedWithNewCollection =
await encryptWithNewCollectionKey(collection, files);
const requestBody: AddToCollectionRequest = {
collectionID: collection.id,
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${ENDPOINT}/collections/add-files`,
2021-09-20 06:21:31 +00:00
requestBody,
null,
2021-09-20 06:21:31 +00:00
{
'X-Auth-Token': token,
}
);
2021-01-25 07:05:45 +00:00
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'Add to collection Failed ');
2021-09-10 04:04:38 +00:00
throw e;
2021-01-25 07:05:45 +00:00
}
};
2021-10-04 07:14:45 +00:00
export const restoreToCollection = async (
collection: Collection,
2022-01-04 09:50:14 +00:00
files: EnteFile[]
2021-10-04 07:14:45 +00:00
) => {
try {
const token = getToken();
const fileKeysEncryptedWithNewCollection =
await encryptWithNewCollectionKey(collection, files);
const requestBody: AddToCollectionRequest = {
collectionID: collection.id,
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${ENDPOINT}/collections/restore-files`,
requestBody,
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'restore to collection Failed ');
throw e;
}
};
2021-09-20 06:21:31 +00:00
export const moveToCollection = async (
2021-09-21 12:22:31 +00:00
fromCollectionID: number,
toCollection: Collection,
2022-01-04 09:50:14 +00:00
files: EnteFile[]
2021-09-20 06:21:31 +00:00
) => {
try {
const token = getToken();
const fileKeysEncryptedWithNewCollection =
2021-09-21 12:22:31 +00:00
await encryptWithNewCollectionKey(toCollection, files);
2021-09-20 06:21:31 +00:00
const requestBody: MoveToCollectionRequest = {
2021-09-21 12:22:31 +00:00
fromCollectionID: fromCollectionID,
toCollectionID: toCollection.id,
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${ENDPOINT}/collections/move-files`,
requestBody,
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'move to collection Failed ');
throw e;
}
2021-09-20 06:21:31 +00:00
};
const encryptWithNewCollectionKey = async (
newCollection: Collection,
2022-01-04 09:50:14 +00:00
files: EnteFile[]
2021-09-20 06:21:31 +00:00
): Promise<EncryptedFileKey[]> => {
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
const worker = await new CryptoWorker();
for (const file of files) {
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(
file.key,
newCollection.key
);
file.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce;
fileKeysEncryptedWithNewCollection.push({
id: file.id,
encryptedKey: file.encryptedKey,
keyDecryptionNonce: file.keyDecryptionNonce,
});
}
return fileKeysEncryptedWithNewCollection;
};
2021-09-28 08:25:15 +00:00
export const removeFromCollection = async (
collection: Collection,
2022-01-04 09:50:14 +00:00
files: EnteFile[]
2021-09-28 08:25:15 +00:00
) => {
2021-01-25 07:05:45 +00:00
try {
const token = getToken();
2021-09-28 08:25:15 +00:00
const request: RemoveFromCollectionRequest = {
collectionID: collection.id,
fileIDs: files.map((file) => file.id),
};
await HTTPService.post(
2021-09-28 08:25:15 +00:00
`${ENDPOINT}/collections/v2/remove-files`,
request,
null,
2021-08-13 02:38:38 +00:00
{ 'X-Auth-Token': token }
);
2021-01-25 07:05:45 +00:00
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'remove from collection failed ');
2021-09-10 04:04:38 +00:00
throw e;
2021-01-25 07:05:45 +00:00
}
};
2021-01-20 13:04:27 +00:00
2021-04-24 07:09:37 +00:00
export const deleteCollection = async (
collectionID: number,
2021-04-28 09:56:57 +00:00
syncWithRemote: () => Promise<void>,
redirectToAll: () => void,
2021-08-13 02:38:38 +00:00
setDialogMessage: SetDialogMessage
2021-04-24 07:09:37 +00:00
) => {
try {
const token = getToken();
await HTTPService.delete(
`${ENDPOINT}/collections/v2/${collectionID}`,
2021-04-24 07:09:37 +00:00
null,
null,
2021-08-13 02:38:38 +00:00
{ 'X-Auth-Token': token }
2021-04-24 07:09:37 +00:00
);
2021-04-28 09:56:57 +00:00
await syncWithRemote();
redirectToAll();
2021-04-24 07:09:37 +00:00
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'delete collection failed ');
setDialogMessage({
title: constants.ERROR,
content: constants.DELETE_COLLECTION_FAILED,
2021-05-30 16:56:48 +00:00
close: { variant: 'danger' },
});
2021-02-09 05:07:46 +00:00
}
2021-04-24 07:09:37 +00:00
};
export const renameCollection = async (
collection: Collection,
2021-08-13 02:38:38 +00:00
newCollectionName: string
) => {
const token = getToken();
const worker = await new CryptoWorker();
const {
encryptedData: encryptedName,
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
newCollectionName,
2021-08-13 02:38:38 +00:00
collection.key
);
const collectionRenameRequest = {
collectionID: collection.id,
encryptedName,
nameDecryptionNonce,
};
await HTTPService.post(
`${ENDPOINT}/collections/rename`,
collectionRenameRequest,
null,
{
'X-Auth-Token': token,
2021-08-13 02:38:38 +00:00
}
);
};
export const shareCollection = async (
collection: Collection,
2021-08-13 02:38:38 +00:00
withUserEmail: string
) => {
try {
2021-04-28 08:00:15 +00:00
const worker = await new CryptoWorker();
const token = getToken();
2021-04-28 08:00:15 +00:00
const publicKey: string = await getPublicKey(withUserEmail);
2021-04-28 09:11:25 +00:00
const encryptedKey: string = await worker.boxSeal(
2021-04-28 08:00:15 +00:00
collection.key,
2021-08-13 02:38:38 +00:00
publicKey
2021-04-28 08:00:15 +00:00
);
const shareCollectionRequest = {
collectionID: collection.id,
email: withUserEmail,
2021-05-29 06:27:52 +00:00
encryptedKey,
};
await HTTPService.post(
`${ENDPOINT}/collections/share`,
shareCollectionRequest,
null,
{
'X-Auth-Token': token,
2021-08-13 02:38:38 +00:00
}
);
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'share collection failed ');
throw e;
}
};
export const unshareCollection = async (
collection: Collection,
2021-08-13 02:38:38 +00:00
withUserEmail: string
) => {
try {
const token = getToken();
const shareCollectionRequest = {
collectionID: collection.id,
email: withUserEmail,
};
await HTTPService.post(
`${ENDPOINT}/collections/unshare`,
shareCollectionRequest,
null,
{
'X-Auth-Token': token,
2021-08-13 02:38:38 +00:00
}
);
} catch (e) {
2021-06-12 17:14:21 +00:00
logError(e, 'unshare collection failed ');
}
};
2022-01-20 06:42:27 +00:00
export const createShareableURL = async (collection: Collection) => {
try {
const token = getToken();
2022-01-19 10:53:59 +00:00
if (!token) {
throw Error(CustomError.TOKEN_MISSING);
}
const createPublicAccessTokenRequest: CreatePublicAccessTokenRequest = {
collectionID: collection.id,
validTill:
Date.now() * 1000 + COLLECTION_SHARE_DEFAULT_VALID_DURATION,
deviceLimit: COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT,
};
const resp = await HTTPService.post(
`${ENDPOINT}/collections/share-url`,
createPublicAccessTokenRequest,
null,
{
'X-Auth-Token': token,
}
);
2022-01-24 11:03:33 +00:00
return resp.data.result as PublicURL;
} catch (e) {
2022-01-20 06:42:27 +00:00
logError(e, 'createShareableURL failed ');
2022-01-19 10:53:59 +00:00
throw e;
}
};
export const deleteShareableURL = async (collection: Collection) => {
try {
const token = getToken();
if (!token) {
throw Error(CustomError.TOKEN_MISSING);
}
await HTTPService.delete(
`${ENDPOINT}/collections/share-url/${collection.id}`,
null,
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
2022-01-20 06:42:27 +00:00
logError(e, 'deleteShareableURL failed ');
throw e;
}
};
2021-04-24 07:09:37 +00:00
export const getFavCollection = async () => {
const collections = await getLocalCollections();
2021-05-29 06:27:52 +00:00
for (const collection of collections) {
if (collection.type === CollectionType.favorites) {
2021-04-24 07:09:37 +00:00
return collection;
}
2021-02-09 05:07:46 +00:00
}
2021-04-24 07:09:37 +00:00
return null;
};
2021-03-15 17:30:49 +00:00
export const getNonEmptyCollections = (
collections: Collection[],
2022-01-04 09:50:14 +00:00
files: EnteFile[]
2021-03-15 17:30:49 +00:00
) => {
const nonEmptyCollectionsIds = new Set<number>();
2021-05-29 06:27:52 +00:00
for (const file of files) {
2021-03-15 17:30:49 +00:00
nonEmptyCollectionsIds.add(file.collectionID);
}
2021-08-13 02:38:38 +00:00
return collections.filter((collection) =>
nonEmptyCollectionsIds.has(collection.id)
);
2021-03-15 17:30:49 +00:00
};
export function sortCollections(
collections: Collection[],
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
sortBy: COLLECTION_SORT_BY
) {
2021-09-30 07:32:58 +00:00
return moveFavCollectionToFront(
collections.sort((collectionA, collectionB) => {
switch (sortBy) {
case COLLECTION_SORT_BY.LATEST_FILE:
return compareCollectionsLatestFile(
collectionAndTheirLatestFile,
collectionA,
collectionB
);
case COLLECTION_SORT_BY.MODIFICATION_TIME:
return collectionB.updationTime - collectionA.updationTime;
case COLLECTION_SORT_BY.NAME:
return collectionA.name.localeCompare(collectionB.name);
}
})
);
}
function compareCollectionsLatestFile(
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
collectionA: Collection,
collectionB: Collection
) {
2021-09-30 07:32:58 +00:00
if (!collectionAndTheirLatestFile?.length) {
return 0;
}
const CollectionALatestFile = getCollectionLatestFile(
collectionAndTheirLatestFile,
collectionA
);
const CollectionBLatestFile = getCollectionLatestFile(
collectionAndTheirLatestFile,
collectionB
);
2021-09-28 13:51:54 +00:00
if (!CollectionALatestFile || !CollectionBLatestFile) {
return 0;
} else {
const sortedFiles = sortFiles([
CollectionALatestFile,
CollectionBLatestFile,
]);
if (sortedFiles[0].id !== CollectionALatestFile.id) {
return 1;
} else {
return -1;
}
}
}
function getCollectionLatestFile(
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
collection: Collection
) {
const collectionAndItsLatestFile = collectionAndTheirLatestFile.filter(
(collectionAndItsLatestFile) =>
collectionAndItsLatestFile.collection.id === collection.id
);
if (collectionAndItsLatestFile.length === 1) {
return collectionAndItsLatestFile[0].file;
}
}
2021-09-30 07:32:58 +00:00
function moveFavCollectionToFront(collections: Collection[]) {
return collections.sort((collectionA, collectionB) =>
collectionA.type === CollectionType.favorites
? -1
: collectionB.type === CollectionType.favorites
? 1
: 0
);
}