Merge branch 'main' into update-is-sentry-enabled

This commit is contained in:
Abhinav 2023-12-05 14:14:10 +05:30
commit a8a7a1b37f
58 changed files with 843 additions and 401 deletions

View file

@ -1,4 +1,11 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
branch="$(git rev-parse --abbrev-ref HEAD)"
if [ "$branch" = "main" ]; then
echo "You can't commit directly to main branch"
exit 1
fi
yarn lint-staged

View file

@ -33,7 +33,7 @@ const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
<p
style={{
fontWeight: 'bold',
marginBottom: '0px',
margin: '0px',
fontSize: '14px',
textAlign: 'left',
}}>
@ -41,6 +41,7 @@ const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
</p>
<p
style={{
marginTop: '0px',
marginBottom: '8px',
textAlign: 'left',
fontSize: '12px',
@ -52,6 +53,8 @@ const TOTPDisplay = ({ issuer, account, code, nextCode, period }) => {
</p>
<p
style={{
margin: '0px',
marginBottom: '1rem',
fontSize: '24px',
fontWeight: 'bold',
textAlign: 'left',

View file

@ -27,7 +27,7 @@
"bs58": "^5.0.0",
"chrono-node": "^2.2.6",
"comlink": "^4.3.0",
"debounce-promise": "^3.1.2",
"debounce": "^2.0.0",
"density-clustering": "^1.3.0",
"eventemitter3": "^4.0.7",
"exifr": "^7.1.3",
@ -37,7 +37,7 @@
"formik": "^2.1.5",
"get-user-locale": "^2.1.3",
"hdbscan": "0.0.1-alpha.5",
"heic-convert": "^1.2.4",
"heic-convert": "^2.0.0",
"i18next": "^23.4.1",
"i18next-http-backend": "^2.1.1",
"idb": "^7.1.1",
@ -51,6 +51,7 @@
"ml-matrix": "^6.10.4",
"next-transpile-modules": "^10.0.0",
"otpauth": "^9.0.2",
"p-debounce": "^4.0.0",
"p-queue": "^7.1.0",
"photoswipe": "file:./thirdparty/photoswipe",
"piexifjs": "^1.0.6",
@ -81,7 +82,6 @@
"devDependencies": {
"@next/bundle-analyzer": "^13.4.12",
"@types/bs58": "^4.0.1",
"@types/debounce-promise": "^3.1.3",
"@types/leaflet": "^1.9.3",
"@types/libsodium-wrappers": "^0.7.8",
"@types/node": "^14.6.4",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generierung von Verschlüsselungsschlüsseln...",
"PASSPHRASE_HINT": "Passwort",
"CONFIRM_PASSPHRASE": "Passwort bestätigen",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "Die Passwörter stimmen nicht überein",
"CONSOLE_WARNING_STOP": "STOPP!",
"CONSOLE_WARNING_DESC": "",
@ -423,7 +425,6 @@
"FILES": "Dateien",
"EACH": "",
"DEDUPLICATE_BASED_ON_SIZE": "",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
"STOP_ALL_UPLOADS_MESSAGE": "",
"STOP_UPLOADS_HEADER": "Hochladen stoppen?",
"YES_STOP_UPLOADS": "Ja, Hochladen stoppen",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generating encryption keys...",
"PASSPHRASE_HINT": "Password",
"CONFIRM_PASSPHRASE": "Confirm password",
"REFERRAL_CODE_HINT":"How did you hear about Ente? (optional)",
"REFERRAL_INFO":"We don't track app installs, It'd help us if you told us where you found us!",
"PASSPHRASE_MATCH_ERROR": "Passwords don't match",
"CONSOLE_WARNING_STOP": "STOP!",
"CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.",
@ -423,7 +425,6 @@
"FILES": "Files",
"EACH": "Each",
"DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "The following files were clubbed based on their sizes and capture time, please review and delete items you believe are duplicates",
"STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?",
"STOP_UPLOADS_HEADER": "Stop uploads?",
"YES_STOP_UPLOADS": "Yes, stop uploads",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generando claves de encriptación...",
"PASSPHRASE_HINT": "Contraseña",
"CONFIRM_PASSPHRASE": "Confirmar contraseña",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "Las contraseñas no coinciden",
"CONSOLE_WARNING_STOP": "STOP!",
"CONSOLE_WARNING_DESC": "Esta es una característica del navegador destinada a los desarrolladores. Por favor, no copie y pegue código sin verificar aquí.",
@ -423,7 +425,6 @@
"FILES": "Archivos",
"EACH": "Cada",
"DEDUPLICATE_BASED_ON_SIZE": "Los siguientes archivos fueron organizados en base a sus tamaños, por favor revise y elimine elementos que cree que son duplicados",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "Los siguientes archivos fueron organizados en base a sus tamaños y tiempo de captura, por favor revise y elimine elementos que cree que son duplicados",
"STOP_ALL_UPLOADS_MESSAGE": "¿Está seguro que desea detener todas las subidas en curso?",
"STOP_UPLOADS_HEADER": "Detener las subidas?",
"YES_STOP_UPLOADS": "Sí, detener las subidas",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
"PASSPHRASE_HINT": "",
"CONFIRM_PASSPHRASE": "",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "",
"CONSOLE_WARNING_STOP": "",
"CONSOLE_WARNING_DESC": "",
@ -423,7 +425,6 @@
"FILES": "",
"EACH": "",
"DEDUPLICATE_BASED_ON_SIZE": "",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
"STOP_ALL_UPLOADS_MESSAGE": "",
"STOP_UPLOADS_HEADER": "",
"YES_STOP_UPLOADS": "",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
"PASSPHRASE_HINT": "",
"CONFIRM_PASSPHRASE": "",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "",
"CONSOLE_WARNING_STOP": "",
"CONSOLE_WARNING_DESC": "",
@ -423,7 +425,6 @@
"FILES": "",
"EACH": "",
"DEDUPLICATE_BASED_ON_SIZE": "",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
"STOP_ALL_UPLOADS_MESSAGE": "",
"STOP_UPLOADS_HEADER": "",
"YES_STOP_UPLOADS": "",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Génération des clés de chiffrement...",
"PASSPHRASE_HINT": "Mot de passe",
"CONFIRM_PASSPHRASE": "Confirmer le mot de passe",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "Les mots de passe ne correspondent pas",
"CONSOLE_WARNING_STOP": "STOP!",
"CONSOLE_WARNING_DESC": "Ceci est une fonction de navigateur dédiée aux développeurs. Veuillez ne pas copier-coller un code non vérifié à cet endroit.",
@ -423,7 +425,6 @@
"FILES": "Fichiers",
"EACH": "Chacun",
"DEDUPLICATE_BASED_ON_SIZE": "Les fichiers suivants ont été clubbed, basé sur leurs tailles, veuillez corriger et supprimer les objets que vous pensez être dupliqués",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "Les fichiers suivants ont été clubbed, basé sur leurs tailles et de l'heure de capture, veuillez corriger et supprimer les objets que vous pensez être dupliqués",
"STOP_ALL_UPLOADS_MESSAGE": "Êtes-vous certains de vouloir arrêter tous les chargements en cours?",
"STOP_UPLOADS_HEADER": "Arrêter les chargements ?",
"YES_STOP_UPLOADS": "Oui, arrêter tout",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generazione delle chiavi di crittografia...",
"PASSPHRASE_HINT": "Password",
"CONFIRM_PASSPHRASE": "Conferma la password",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "Le password non corrispondono",
"CONSOLE_WARNING_STOP": "STOP!",
"CONSOLE_WARNING_DESC": "Questa è una funzionalità del browser destinata agli sviluppatori. Non copiare né incollare codice non verificato qui.",
@ -423,7 +425,6 @@
"FILES": "",
"EACH": "",
"DEDUPLICATE_BASED_ON_SIZE": "",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
"STOP_ALL_UPLOADS_MESSAGE": "",
"STOP_UPLOADS_HEADER": "",
"YES_STOP_UPLOADS": "",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "Encryptiecodes worden gegenereerd...",
"PASSPHRASE_HINT": "Wachtwoord",
"CONFIRM_PASSPHRASE": "Wachtwoord bevestigen",
"REFERRAL_CODE_HINT": "Hoe hoorde je over Ente? (optioneel)",
"REFERRAL_INFO": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!",
"PASSPHRASE_MATCH_ERROR": "Wachtwoorden komen niet overeen",
"CONSOLE_WARNING_STOP": "STOP!",
"CONSOLE_WARNING_DESC": "Dit is een browserfunctie bedoeld voor ontwikkelaars. Gelieve hier geen niet-geverifieerde code te kopiëren/plakken.",
@ -157,7 +159,7 @@
"RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Vernieuwt op {{date, dateTime}}",
"RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Eindigt op {{date, dateTime}}",
"RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Uw abonnement loopt af op {{date, dateTime}}",
"ADD_ON_AVAILABLE_TILL": "",
"ADD_ON_AVAILABLE_TILL": "Jouw {{storage, string}} add-on is geldig tot {{date, dateTime}}",
"STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "U heeft uw opslaglimiet overschreden, gelieve <a>upgraden</a>",
"SUBSCRIPTION_PURCHASE_SUCCESS": "<p>We hebben uw betaling ontvangen</p><p>Uw abonnement is geldig tot <strong>{{date, dateTime}}</strong></p>",
"SUBSCRIPTION_PURCHASE_CANCELLED": "Uw aankoop is geannuleerd, probeer het opnieuw als u zich wilt abonneren",
@ -172,7 +174,7 @@
"UPDATE_SUBSCRIPTION": "Abonnement wijzigen",
"CANCEL_SUBSCRIPTION": "Abonnement opzeggen",
"CANCEL_SUBSCRIPTION_MESSAGE": "<p>Al je gegevens zullen worden verwijderd van onze servers aan het einde van deze factureringsperiode.</p><p>Weet u zeker dat u uw abonnement wilt opzeggen?</p>",
"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "",
"CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "<p>Weet je zeker dat je je abonnement wilt opzeggen?</p>",
"SUBSCRIPTION_CANCEL_FAILED": "Abonnement opzeggen mislukt",
"SUBSCRIPTION_CANCEL_SUCCESS": "Abonnement succesvol geannuleerd",
"REACTIVATE_SUBSCRIPTION": "Abonnement opnieuw activeren",
@ -423,7 +425,6 @@
"FILES": "Bestanden",
"EACH": "Elke",
"DEDUPLICATE_BASED_ON_SIZE": "De volgende bestanden zijn samengevoegd op basis van hun groottes. Controleer en verwijder items waarvan je denkt dat ze dubbel zijn",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "De volgende bestanden zijn samengevoegd op basis van hun groottes en opnametijd, bekijk en verwijder items waarvan je denkt dat ze dubbel zijn",
"STOP_ALL_UPLOADS_MESSAGE": "Weet u zeker dat u wilt stoppen met alle uploads die worden uitgevoerd?",
"STOP_UPLOADS_HEADER": "Stoppen met uploaden?",
"YES_STOP_UPLOADS": "Ja, stop uploaden",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
"PASSPHRASE_HINT": "",
"CONFIRM_PASSPHRASE": "",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "",
"CONSOLE_WARNING_STOP": "PARAR!",
"CONSOLE_WARNING_DESC": "",
@ -423,7 +425,6 @@
"FILES": "",
"EACH": "",
"DEDUPLICATE_BASED_ON_SIZE": "",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
"STOP_ALL_UPLOADS_MESSAGE": "",
"STOP_UPLOADS_HEADER": "",
"YES_STOP_UPLOADS": "",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
"PASSPHRASE_HINT": "",
"CONFIRM_PASSPHRASE": "",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "",
"CONSOLE_WARNING_STOP": "",
"CONSOLE_WARNING_DESC": "",
@ -423,7 +425,6 @@
"FILES": "",
"EACH": "",
"DEDUPLICATE_BASED_ON_SIZE": "",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
"STOP_ALL_UPLOADS_MESSAGE": "",
"STOP_UPLOADS_HEADER": "",
"YES_STOP_UPLOADS": "",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "",
"PASSPHRASE_HINT": "",
"CONFIRM_PASSPHRASE": "",
"REFERRAL_CODE_HINT": "",
"REFERRAL_INFO": "",
"PASSPHRASE_MATCH_ERROR": "",
"CONSOLE_WARNING_STOP": "",
"CONSOLE_WARNING_DESC": "",
@ -423,7 +425,6 @@
"FILES": "",
"EACH": "",
"DEDUPLICATE_BASED_ON_SIZE": "",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "",
"STOP_ALL_UPLOADS_MESSAGE": "",
"STOP_UPLOADS_HEADER": "",
"YES_STOP_UPLOADS": "",

View file

@ -38,6 +38,8 @@
"KEY_GENERATION_IN_PROGRESS_MESSAGE": "正在生成加密密钥...",
"PASSPHRASE_HINT": "密码",
"CONFIRM_PASSPHRASE": "请确认密码",
"REFERRAL_CODE_HINT": "您是如何知道Ente的 (可选的)",
"REFERRAL_INFO": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
"PASSPHRASE_MATCH_ERROR": "两次输入的密码不一致",
"CONSOLE_WARNING_STOP": "停止!",
"CONSOLE_WARNING_DESC": "这是专为开发人员设计的浏览器功能。 请不要在此处复制粘贴未经验证的代码。",
@ -423,7 +425,6 @@
"FILES": "文件",
"EACH": "每个",
"DEDUPLICATE_BASED_ON_SIZE": "以下文件根据大小进行了合并,请检查并删除您认为重复的项目",
"DEDUPLICATE_BASED_ON_SIZE_AND_CAPTURE_TIME": "以下文件是根据它们的大小和捕获时间合并的,请检查并删除您认为重复的项目",
"STOP_ALL_UPLOADS_MESSAGE": "您确定要停止所有正在进行的上传吗?",
"STOP_UPLOADS_HEADER": "要停止上传吗?",
"YES_STOP_UPLOADS": "是的,停止上传",

View file

@ -8,7 +8,6 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import PhotoViewer from 'components/PhotoViewer';
import { TRASH_SECTION } from 'constants/collection';
import { updateFileMsrcProps, updateFileSrcProps } from 'utils/photoFrame';
import { PhotoList } from './PhotoList';
import { MergedSourceURL, SelectedState } from 'types/gallery';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
@ -19,6 +18,10 @@ import PhotoSwipe from 'photoswipe';
import useMemoSingleThreaded from '@ente/shared/hooks/useMemoSingleThreaded';
import { getPlayableVideo } from 'utils/file';
import { FILE_TYPE } from 'constants/file';
import { PHOTOS_PAGES } from '@ente/shared/constants/pages';
import { PhotoList } from './PhotoList';
import { DedupePhotoList } from './PhotoList/dedupe';
import { Duplicate } from 'services/deduplicationService';
const Container = styled('div')`
display: block;
@ -36,7 +39,12 @@ const Container = styled('div')`
const PHOTOSWIPE_HASH_SUFFIX = '&opened';
interface Props {
page:
| PHOTOS_PAGES.GALLERY
| PHOTOS_PAGES.DEDUPLICATE
| PHOTOS_PAGES.SHARED_ALBUMS;
files: EnteFile[];
duplicates?: Duplicate[];
syncWithRemote: () => Promise<void>;
favItemIds?: Set<number>;
setSelected: (
@ -55,6 +63,8 @@ interface Props {
}
const PhotoFrame = ({
page,
duplicates,
files,
syncWithRemote,
favItemIds,
@ -605,7 +615,17 @@ const PhotoFrame = ({
return (
<Container>
<AutoSizer>
{({ height, width }) => (
{({ height, width }) =>
page === PHOTOS_PAGES.DEDUPLICATE ? (
<DedupePhotoList
width={width}
height={height}
getThumbnail={getThumbnail}
duplicates={duplicates}
activeCollectionID={activeCollectionID}
showAppDownloadBanner={showAppDownloadBanner}
/>
) : (
<PhotoList
width={width}
height={height}
@ -614,7 +634,8 @@ const PhotoFrame = ({
activeCollectionID={activeCollectionID}
showAppDownloadBanner={showAppDownloadBanner}
/>
)}
)
}
</AutoSizer>
<PhotoViewer
isOpen={open}

View file

@ -0,0 +1,365 @@
import React, { useRef, useEffect, useState, useMemo } from 'react';
import {
VariableSizeList as List,
ListChildComponentProps,
areEqual,
} from 'react-window';
import { Box, styled } from '@mui/material';
import { EnteFile } from 'types/file';
import {
IMAGE_CONTAINER_MAX_HEIGHT,
MIN_COLUMNS,
DATE_CONTAINER_HEIGHT,
GAP_BTW_TILES,
SPACE_BTW_DATES,
SIZE_AND_COUNT_CONTAINER_HEIGHT,
IMAGE_CONTAINER_MAX_WIDTH,
} from 'constants/gallery';
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
import { FlexWrapper } from '@ente/shared/components/Container';
import { t } from 'i18next';
import memoize from 'memoize-one';
import { Duplicate } from 'services/deduplicationService';
export enum ITEM_TYPE {
TIME = 'TIME',
FILE = 'FILE',
SIZE_AND_COUNT = 'SIZE_AND_COUNT',
HEADER = 'HEADER',
FOOTER = 'FOOTER',
MARKETING_FOOTER = 'MARKETING_FOOTER',
OTHER = 'OTHER',
}
export interface TimeStampListItem {
itemType: ITEM_TYPE;
items?: EnteFile[];
itemStartIndex?: number;
date?: string;
dates?: {
date: string;
span: number;
}[];
groups?: number[];
item?: any;
id?: string;
height?: number;
fileSize?: number;
fileCount?: number;
}
const ListItem = styled('div')`
display: flex;
justify-content: center;
`;
const getTemplateColumns = (
columns: number,
shrinkRatio: number,
groups?: number[]
): string => {
if (groups) {
// need to confirm why this was there
// const sum = groups.reduce((acc, item) => acc + item, 0);
// if (sum < columns) {
// groups[groups.length - 1] += columns - sum;
// }
return groups
.map(
(x) =>
`repeat(${x}, ${IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio}px)`
)
.join(` ${SPACE_BTW_DATES}px `);
} else {
return `repeat(${columns},${
IMAGE_CONTAINER_MAX_WIDTH * shrinkRatio
}px)`;
}
};
function getFractionFittableColumns(width: number): number {
return (
(width - 2 * getGapFromScreenEdge(width) + GAP_BTW_TILES) /
(IMAGE_CONTAINER_MAX_WIDTH + GAP_BTW_TILES)
);
}
function getGapFromScreenEdge(width: number) {
if (width > MIN_COLUMNS * IMAGE_CONTAINER_MAX_WIDTH) {
return 24;
} else {
return 4;
}
}
function getShrinkRatio(width: number, columns: number) {
return (
(width -
2 * getGapFromScreenEdge(width) -
(columns - 1) * GAP_BTW_TILES) /
(columns * IMAGE_CONTAINER_MAX_WIDTH)
);
}
const ListContainer = styled(Box)<{
columns: number;
shrinkRatio: number;
groups?: number[];
}>`
display: grid;
grid-template-columns: ${({ columns, shrinkRatio, groups }) =>
getTemplateColumns(columns, shrinkRatio, groups)};
grid-column-gap: ${GAP_BTW_TILES}px;
width: 100%;
color: #fff;
padding: 0 24px;
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
padding: 0 4px;
}
`;
const ListItemContainer = styled(FlexWrapper)<{ span: number }>`
grid-column: span ${(props) => props.span};
`;
const DateContainer = styled(ListItemContainer)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
height: ${DATE_CONTAINER_HEIGHT}px;
color: ${({ theme }) => theme.colors.text.muted};
`;
const SizeAndCountContainer = styled(DateContainer)`
margin-top: 1rem;
height: ${SIZE_AND_COUNT_CONTAINER_HEIGHT}px;
`;
interface Props {
height: number;
width: number;
duplicates: Duplicate[];
showAppDownloadBanner: boolean;
getThumbnail: (
file: EnteFile,
index: number,
isScrolling?: boolean
) => JSX.Element;
activeCollectionID: number;
}
interface ItemData {
timeStampList: TimeStampListItem[];
columns: number;
shrinkRatio: number;
renderListItem: (
timeStampListItem: TimeStampListItem,
isScrolling?: boolean
) => JSX.Element;
}
const createItemData = memoize(
(
timeStampList: TimeStampListItem[],
columns: number,
shrinkRatio: number,
renderListItem: (
timeStampListItem: TimeStampListItem,
isScrolling?: boolean
) => JSX.Element
): ItemData => ({
timeStampList,
columns,
shrinkRatio,
renderListItem,
})
);
const PhotoListRow = React.memo(
({
index,
style,
isScrolling,
data,
}: ListChildComponentProps<ItemData>) => {
const { timeStampList, columns, shrinkRatio, renderListItem } = data;
return (
<ListItem style={style}>
<ListContainer
columns={columns}
shrinkRatio={shrinkRatio}
groups={timeStampList[index].groups}>
{renderListItem(timeStampList[index], isScrolling)}
</ListContainer>
</ListItem>
);
},
areEqual
);
const getTimeStampListFromDuplicates = (duplicates: Duplicate[], columns) => {
const timeStampList: TimeStampListItem[] = [];
for (let index = 0; index < duplicates.length; index++) {
const dupes = duplicates[index];
timeStampList.push({
itemType: ITEM_TYPE.SIZE_AND_COUNT,
fileSize: dupes.size,
fileCount: dupes.files.length,
});
let lastIndex = 0;
while (lastIndex < dupes.files.length) {
timeStampList.push({
itemType: ITEM_TYPE.FILE,
items: dupes.files.slice(lastIndex, lastIndex + columns),
itemStartIndex: index,
});
lastIndex += columns;
}
}
return timeStampList;
};
export function DedupePhotoList({
height,
width,
duplicates,
getThumbnail,
activeCollectionID,
}: Props) {
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
const refreshInProgress = useRef(false);
const shouldRefresh = useRef(false);
const listRef = useRef(null);
const columns = useMemo(() => {
const fittableColumns = getFractionFittableColumns(width);
let columns = Math.floor(fittableColumns);
if (columns < MIN_COLUMNS) {
columns = MIN_COLUMNS;
}
return columns;
}, [width]);
const shrinkRatio = getShrinkRatio(width, columns);
const listItemHeight =
IMAGE_CONTAINER_MAX_HEIGHT * shrinkRatio + GAP_BTW_TILES;
const refreshList = () => {
listRef.current?.resetAfterIndex(0);
};
useEffect(() => {
const main = () => {
if (refreshInProgress.current) {
shouldRefresh.current = true;
return;
}
refreshInProgress.current = true;
const timeStampList = getTimeStampListFromDuplicates(
duplicates,
columns
);
setTimeStampList(timeStampList);
refreshInProgress.current = false;
if (shouldRefresh.current) {
shouldRefresh.current = false;
setTimeout(main, 0);
}
};
main();
}, [columns, duplicates]);
useEffect(() => {
refreshList();
}, [timeStampList]);
const getItemSize = (timeStampList) => (index) => {
switch (timeStampList[index].itemType) {
case ITEM_TYPE.TIME:
return DATE_CONTAINER_HEIGHT;
case ITEM_TYPE.SIZE_AND_COUNT:
return SIZE_AND_COUNT_CONTAINER_HEIGHT;
case ITEM_TYPE.FILE:
return listItemHeight;
default:
return timeStampList[index].height;
}
};
const generateKey = (index) => {
switch (timeStampList[index].itemType) {
case ITEM_TYPE.FILE:
return `${timeStampList[index].items[0].id}-${
timeStampList[index].items.slice(-1)[0].id
}`;
default:
return `${timeStampList[index].id}-${index}`;
}
};
const renderListItem = (
listItem: TimeStampListItem,
isScrolling: boolean
) => {
switch (listItem.itemType) {
case ITEM_TYPE.SIZE_AND_COUNT:
return (
<SizeAndCountContainer span={columns}>
{listItem.fileCount} {t('FILES')},{' '}
{convertBytesToHumanReadable(listItem.fileSize || 0)}{' '}
{t('EACH')}
</SizeAndCountContainer>
);
case ITEM_TYPE.FILE: {
const ret = listItem.items.map((item, idx) =>
getThumbnail(
item,
listItem.itemStartIndex + idx,
isScrolling
)
);
if (listItem.groups) {
let sum = 0;
for (let i = 0; i < listItem.groups.length - 1; i++) {
sum = sum + listItem.groups[i];
ret.splice(
sum,
0,
<div key={`${listItem.items[0].id}-gap-${i}`} />
);
sum += 1;
}
}
return ret;
}
default:
return listItem.item;
}
};
if (!timeStampList?.length) {
return <></>;
}
const itemData = createItemData(
timeStampList,
columns,
shrinkRatio,
renderListItem
);
return (
<List
key={`${activeCollectionID}`}
itemData={itemData}
ref={listRef}
itemSize={getItemSize(timeStampList)}
height={height}
width={width}
itemCount={timeStampList.length}
itemKey={generateKey}
overscanCount={3}
useIsScrolling>
{PhotoListRow}
</List>
);
}

View file

@ -19,14 +19,12 @@ import {
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { ENTE_WEBSITE_LINK } from '@ente/shared/constants/urls';
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
import { DeduplicateContext } from 'pages/deduplicate';
import { FlexWrapper } from '@ente/shared/components/Container';
import { Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery';
import { formatDate } from '@ente/shared/time/format';
import { Trans } from 'react-i18next';
import { t } from 'i18next';
import { areFilesWithFileHashSame, hasFileHash } from 'utils/upload';
import memoize from 'memoize-one';
const A_DAY = 24 * 60 * 60 * 1000;
@ -261,7 +259,6 @@ export function PhotoList({
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const deduplicateContext = useContext(DeduplicateContext);
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
const refreshInProgress = useRef(false);
@ -306,9 +303,6 @@ export function PhotoList({
}
if (galleryContext.isClipSearchResult) {
noGrouping(timeStampList);
} else if (deduplicateContext.isOnDeduplicatePage) {
skipMerge = true;
groupByFileSize(timeStampList);
} else {
groupByTime(timeStampList);
}
@ -345,9 +339,6 @@ export function PhotoList({
width,
height,
displayFiles,
deduplicateContext.isOnDeduplicatePage,
deduplicateContext.fileSizeMap,
deduplicateContext.clubSameTimeFilesOnly,
galleryContext.photoListHeader,
publicCollectionGalleryContext.photoListHeader,
galleryContext.isClipSearchResult,
@ -420,67 +411,6 @@ export function PhotoList({
refreshList();
}, [timeStampList]);
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
let index = 0;
while (index < displayFiles.length) {
const firstFile = displayFiles[index];
const firstFileSize = deduplicateContext.fileSizeMap.get(
firstFile.id
);
const firstFileCreationTime = firstFile.metadata.creationTime;
let lastFileIndex = index;
while (lastFileIndex < displayFiles.length) {
const lastFile = displayFiles[lastFileIndex];
const lastFileSize = deduplicateContext.fileSizeMap.get(
lastFile.id
);
if (lastFileSize !== firstFileSize) {
break;
}
const lastFileCreationTime = lastFile.metadata.creationTime;
if (
deduplicateContext.clubSameTimeFilesOnly &&
lastFileCreationTime !== firstFileCreationTime
) {
break;
}
const eitherFileHasFileHash =
hasFileHash(lastFile.metadata) ||
hasFileHash(firstFile.metadata);
if (
eitherFileHasFileHash &&
!areFilesWithFileHashSame(
lastFile.metadata,
firstFile.metadata
)
) {
break;
}
lastFileIndex++;
}
lastFileIndex--;
timeStampList.push({
itemType: ITEM_TYPE.SIZE_AND_COUNT,
fileSize: firstFileSize,
fileCount: lastFileIndex - index + 1,
});
while (index <= lastFileIndex) {
const tileSize = Math.min(columns, lastFileIndex - index + 1);
timeStampList.push({
itemType: ITEM_TYPE.FILE,
items: displayFiles.slice(index, index + tileSize),
itemStartIndex: index,
});
index += tileSize;
}
}
};
const groupByTime = (timeStampList: TimeStampListItem[]) => {
let listItemIndex = 0;
let currentDate;

View file

@ -137,6 +137,8 @@ function PhotoViewer(props: Iprops) {
const [showEditButton, setShowEditButton] = useState(false);
const [showZoomButton, setShowZoomButton] = useState(false);
useEffect(() => {
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
publicCollectionDownloadManager.setProgressUpdater(
@ -338,6 +340,10 @@ function PhotoViewer(props: Iprops) {
);
}
function updateShowZoomButton(file: EnteFile) {
setShowZoomButton(file.metadata.fileType === FILE_TYPE.IMAGE);
}
const openPhotoSwipe = () => {
const { items, currentIndex } = props;
const options = {
@ -411,6 +417,7 @@ function PhotoViewer(props: Iprops) {
updateShowConvertBtn(currItem);
updateIsSourceLoaded(currItem);
updateShowEditButton(currItem);
updateShowZoomButton(currItem);
});
photoSwipe.listen('resize', () => {
if (!photoSwipe?.currItem) return;
@ -766,12 +773,14 @@ function PhotoViewer(props: Iprops) {
<DeleteIcon />
</button>
)}
{showZoomButton && (
<button
className="pswp__button pswp__button--custom"
onClick={toggleZoomInAndOut}
title={t('ZOOM_IN_OUT')}>
<ZoomInOutlinedIcon />
</button>
)}
<button
className="pswp__button pswp__button--custom"
onClick={() => {

View file

@ -1,5 +1,5 @@
import { IconButton } from '@mui/material';
import debounce from 'debounce-promise';
import pDebounce from 'p-debounce';
import { AppContext } from 'pages/_app';
import React, {
useCallback,
@ -83,7 +83,7 @@ export default function SearchInput(props: Iprops) {
}
};
const getOptions = debounce(
const getOptions = pDebounce(
getAutoCompleteSuggestions(props.files, props.collections),
250
);

View file

@ -64,8 +64,6 @@ export default function UtilitySection({ closeSidebar }) {
const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE);
const redirectToAuthenticatorPage = () => router.push(PAGES.AUTH);
const somethingWentWrong = () =>
setDialogMessage({
title: t('ERROR'),
@ -132,13 +130,6 @@ export default function UtilitySection({ closeSidebar }) {
label={t('DEDUPLICATE_FILES')}
/>
{isInternalUser() && (
<EnteMenuItem
variant="secondary"
onClick={redirectToAuthenticatorPage}
label={t('AUTHENTICATOR_SECTION')}
/>
)}
<EnteMenuItem
variant="secondary"
onClick={openPreferencesOptions}

View file

@ -12,6 +12,7 @@ import { UPLOAD_STRATEGY } from 'constants/upload';
import { getImportSuggestion } from 'utils/upload';
import ElectronAPIs from '@ente/shared/electron';
import { PICKED_UPLOAD_TYPE } from 'constants/upload';
import isElectron from 'is-electron';
interface Iprops {
open: boolean;
@ -25,6 +26,9 @@ export default function WatchFolder({ open, onClose }: Iprops) {
const appContext = useContext(AppContext);
useEffect(() => {
if (!isElectron()) {
return;
}
setMappings(watchFolderService.getWatchMappings());
}, []);

View file

@ -7,12 +7,14 @@ import { PeriodToggler } from '../periodToggler';
import Plans from '../plans';
import { hasAddOnBonus } from 'utils/billing';
import { BFAddOnRow } from '../plans/BfAddOnRow';
import { ManageSubscription } from '../manageSubscription';
export default function FreeSubscriptionPlanSelectorCard({
plans,
subscription,
bonusData,
closeModal,
setLoading,
planPeriod,
togglePeriod,
onPlanSelect,
@ -48,6 +50,14 @@ export default function FreeSubscriptionPlanSelectorCard({
closeModal={closeModal}
/>
)}
{hasAddOnBonus(bonusData) && (
<ManageSubscription
subscription={subscription}
bonusData={bonusData}
closeModal={closeModal}
setLoading={setLoading}
/>
)}
</Stack>
</Box>
</>

View file

@ -191,6 +191,7 @@ function PlanSelectorCard(props: Props) {
subscription={subscription}
bonusData={bonusData}
closeModal={props.closeModal}
setLoading={props.setLoading}
planPeriod={planPeriod}
togglePeriod={togglePeriod}
onPlanSelect={onPlanSelect}

View file

@ -4,12 +4,8 @@ import PhotoFrame from 'components/PhotoFrame';
import { ALL_SECTION } from 'constants/collection';
import { AppContext } from 'pages/_app';
import { createContext, useContext, useEffect, useState } from 'react';
import {
getDuplicateFiles,
clubDuplicatesByTime,
} from 'services/deduplicationService';
import { syncFiles, trashFiles } from 'services/fileService';
import { EnteFile } from 'types/file';
import { getDuplicates, Duplicate } from 'services/deduplicationService';
import { getLocalFiles, trashFiles } from 'services/fileService';
import { SelectedState } from 'types/gallery';
import { ApiError } from '@ente/shared/error';
@ -24,7 +20,7 @@ import { PHOTOS_PAGES as PAGES } from '@ente/shared/constants/pages';
import router from 'next/router';
import { getKey, SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
import { styled } from '@mui/material';
import { getLatestCollections } from 'services/collectionService';
import { getLocalCollections } from 'services/collectionService';
import EnteSpinner from '@ente/shared/components/EnteSpinner';
import { VerticallyCentered } from '@ente/shared/components/Container';
import Typography from '@mui/material/Typography';
@ -43,9 +39,7 @@ export const Info = styled('div')`
export default function Deduplicate() {
const { setDialogMessage, startLoading, finishLoading, showNavBar } =
useContext(AppContext);
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>(null);
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
const [duplicates, setDuplicates] = useState<Duplicate[]>(null);
const [collectionNameMap, setCollectionNameMap] = useState(
new Map<number, string>()
);
@ -69,31 +63,22 @@ export default function Deduplicate() {
useEffect(() => {
syncWithRemote();
}, [clubSameTimeFilesOnly]);
const fileToCollectionsMap = useMemoSingleThreaded(() => {
return constructFileToCollectionMap(duplicateFiles);
}, [duplicateFiles]);
}, []);
const syncWithRemote = async () => {
startLoading();
const collections = await getLatestCollections();
const collections = await getLocalCollections();
const collectionNameMap = new Map<number, string>();
for (const collection of collections) {
collectionNameMap.set(collection.id, collection.name);
}
setCollectionNameMap(collectionNameMap);
const files = await syncFiles('normal', collections, () => null);
let duplicates = await getDuplicateFiles(files, collectionNameMap);
if (clubSameTimeFilesOnly) {
duplicates = clubDuplicatesByTime(duplicates);
}
const files = await getLocalFiles();
const duplicateFiles = await getDuplicates(files, collectionNameMap);
const currFileSizeMap = new Map<number, number>();
let allDuplicateFiles: EnteFile[] = [];
let toSelectFileIDs: number[] = [];
let count = 0;
for (const dupe of duplicates) {
allDuplicateFiles = [...allDuplicateFiles, ...dupe.files];
for (const dupe of duplicateFiles) {
// select all except first file
toSelectFileIDs = [
...toSelectFileIDs,
@ -105,8 +90,7 @@ export default function Deduplicate() {
currFileSizeMap.set(file.id, dupe.size);
}
}
setDuplicateFiles(allDuplicateFiles);
setFileSizeMap(currFileSizeMap);
setDuplicates(duplicateFiles);
const selectedFiles = {
count: count,
ownCount: count,
@ -119,6 +103,16 @@ export default function Deduplicate() {
finishLoading();
};
const duplicateFiles = useMemoSingleThreaded(() => {
return (duplicates ?? []).reduce((acc, dupe) => {
return [...acc, ...dupe.files];
}, []);
}, [duplicates]);
const fileToCollectionsMap = useMemoSingleThreaded(() => {
return constructFileToCollectionMap(duplicateFiles);
}, [duplicateFiles]);
const deleteFileHelper = async () => {
try {
startLoading();
@ -153,7 +147,7 @@ export default function Deduplicate() {
setSelected({ count: 0, collectionID: 0, ownCount: 0 });
};
if (!duplicateFiles) {
if (!duplicates) {
return (
<VerticallyCentered>
<EnteSpinner />
@ -166,19 +160,10 @@ export default function Deduplicate() {
value={{
...DefaultDeduplicateContext,
collectionNameMap,
clubSameTimeFilesOnly,
setClubSameTimeFilesOnly,
fileSizeMap,
isOnDeduplicatePage: true,
}}>
{duplicateFiles.length > 0 && (
<Info>
{t('DEDUPLICATE_BASED_ON', {
context: clubSameTimeFilesOnly
? 'SIZE_AND_CAPTURE_TIME'
: 'SIZE',
})}
</Info>
<Info>{t('DEDUPLICATE_BASED_ON_SIZE')}</Info>
)}
{duplicateFiles.length === 0 ? (
<VerticallyCentered>
@ -188,7 +173,9 @@ export default function Deduplicate() {
</VerticallyCentered>
) : (
<PhotoFrame
page={PAGES.DEDUPLICATE}
files={duplicateFiles}
duplicates={duplicates}
syncWithRemote={syncWithRemote}
setSelected={setSelected}
selected={selected}

View file

@ -1096,6 +1096,7 @@ export default function Gallery() {
<GalleryEmptyState openUploader={openUploader} />
) : (
<PhotoFrame
page={PAGES.GALLERY}
files={filteredData}
syncWithRemote={syncWithRemote}
favItemIds={favItemIds}

View file

@ -132,7 +132,11 @@ export default function LandingPage() {
const user = getData(LS_KEYS.USER);
let key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key && isElectron()) {
try {
key = await ElectronAPIs.getEncryptionKey();
} catch (e) {
logError(e, 'getEncryptionKey failed');
}
if (key) {
await saveKeyInSessionStore(
SESSION_KEYS.ENCRYPTION_KEY,

View file

@ -463,6 +463,7 @@ export default function PublicCollectionGallery() {
openUploader={openUploader}
/>
<PhotoFrame
page={PAGES.SHARED_ALBUMS}
files={publicFiles}
syncWithRemote={syncWithRemote}
setSelected={() => null}

View file

@ -18,6 +18,7 @@ import { FILE_TYPE } from 'constants/file';
import ComlinkCryptoWorker from '@ente/shared/crypto';
import { Embedding, Model } from 'types/embedding';
import { getToken } from '@ente/shared/storage/localStorage/helpers';
import isElectron from 'is-electron';
const CLIP_EMBEDDING_LENGTH = 512;
@ -48,7 +49,7 @@ class ClipServiceImpl {
}
isPlatformSupported = () => {
return !this.unsupportedPlatform;
return isElectron() && !this.unsupportedPlatform;
};
setupOnFileUploadListener = async () => {
@ -145,6 +146,9 @@ class ClipServiceImpl {
try {
return ElectronAPIs.computeTextEmbedding(text);
} catch (e) {
if (e?.message?.includes(CustomError.UNSUPPORTED_PLATFORM)) {
this.unsupportedPlatform = true;
}
logError(e, 'failed to compute text embedding');
throw e;
}

View file

@ -16,12 +16,12 @@ interface DuplicatesResponse {
}>;
}
interface DuplicateFiles {
export interface Duplicate {
files: EnteFile[];
size: number;
}
export async function getDuplicateFiles(
export async function getDuplicates(
files: EnteFile[],
collectionNameMap: Map<number, string>
) {
@ -33,7 +33,7 @@ export async function getDuplicateFiles(
fileMap.set(file.id, file);
}
let result: DuplicateFiles[] = [];
let result: Duplicate[] = [];
for (const dupe of dupes) {
let duplicateFiles: EnteFile[] = [];
@ -64,8 +64,8 @@ export async function getDuplicateFiles(
}
}
function getDupesGroupedBySameFileHashes(dupe: DuplicateFiles) {
const result: DuplicateFiles[] = [];
function getDupesGroupedBySameFileHashes(dupe: Duplicate) {
const result: Duplicate[] = [];
const fileWithHashes: EnteFile[] = [];
const fileWithoutHashes: EnteFile[] = [];
@ -95,8 +95,8 @@ function getDupesGroupedBySameFileHashes(dupe: DuplicateFiles) {
return result;
}
function groupDupesByFileHashes(dupe: DuplicateFiles) {
const result: DuplicateFiles[] = [];
function groupDupesByFileHashes(dupe: Duplicate) {
const result: Duplicate[] = [];
const filesSortedByFileHash = dupe.files
.map((file) => {
@ -141,51 +141,6 @@ function groupDupesByFileHashes(dupe: DuplicateFiles) {
return result;
}
export function clubDuplicatesByTime(dupes: DuplicateFiles[]) {
const result: DuplicateFiles[] = [];
for (const dupe of dupes) {
let files: EnteFile[] = [];
const creationTimeCounter = new Map<number, number>();
let mostFreqCreationTime = 0;
let mostFreqCreationTimeCount = 0;
for (const file of dupe.files) {
const creationTime = file.metadata.creationTime;
if (creationTimeCounter.has(creationTime)) {
creationTimeCounter.set(
creationTime,
creationTimeCounter.get(creationTime) + 1
);
} else {
creationTimeCounter.set(creationTime, 1);
}
if (
creationTimeCounter.get(creationTime) >
mostFreqCreationTimeCount
) {
mostFreqCreationTime = creationTime;
mostFreqCreationTimeCount =
creationTimeCounter.get(creationTime);
}
files.push(file);
}
files = files.filter((file) => {
return file.metadata.creationTime === mostFreqCreationTime;
});
if (files.length > 1) {
result.push({
files,
size: dupe.size,
});
}
}
return result;
}
async function fetchDuplicateFileIDs() {
try {
const response = await HTTPService.get(

View file

@ -4,13 +4,15 @@ import { logError } from '@ente/shared/sentry';
import { ElectronFile } from 'types/upload';
import { CustomError } from '@ente/shared/error';
import { convertBytesToHumanReadable } from '@ente/shared/utils/size';
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
class ElectronImageProcessorService {
async convertToJPEG(fileBlob: Blob, filename: string): Promise<Blob> {
try {
const startTime = Date.now();
const inputFileData = new Uint8Array(await fileBlob.arrayBuffer());
const convertedFileData = await ElectronAPIs.convertToJPEG(
const convertedFileData =
await WorkerSafeElectronService.convertToJPEG(
inputFileData,
filename
);

View file

@ -1,4 +1,4 @@
import debounce from 'debounce-promise';
import debounce from 'debounce';
import PQueue from 'p-queue';
import { eventBus, Events } from '@ente/shared/events';
import { EnteFile } from 'types/file';

View file

@ -32,6 +32,7 @@ import {
computeClipMatchScore,
getLocalClipImageEmbeddings,
} from './clipService';
import { CustomError } from '@ente/shared/error';
const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']);
@ -290,15 +291,23 @@ async function getThingSuggestion(searchPhrase: string): Promise<Suggestion[]> {
}
async function getClipSuggestion(searchPhrase: string): Promise<Suggestion> {
try {
if (!ClipService.isPlatformSupported()) {
return null;
}
const clipResults = await searchClip(searchPhrase);
return {
type: SuggestionType.CLIP,
value: clipResults,
label: searchPhrase,
};
} catch (e) {
if (!e.message?.includes(CustomError.MODEL_DOWNLOAD_PENDING)) {
logError(e, 'getClipSuggestion failed');
}
return null;
}
}
function searchCollection(

View file

@ -9,7 +9,7 @@ import {
WatchMapping,
WatchMappingSyncedFile,
} from 'types/watchFolder';
import debounce from 'debounce-promise';
import debounce from 'debounce';
import {
diskFileAddedCallback,
diskFileRemovedCallback,
@ -37,6 +37,11 @@ class watchFolderService {
private setCollectionName: (collectionName: string) => void;
private syncWithRemote: () => void;
private setWatchFolderServiceIsRunning: (isRunning: boolean) => void;
private debouncedRunNextEvent: () => void;
constructor() {
this.debouncedRunNextEvent = debounce(() => this.runNextEvent(), 1000);
}
isUploadRunning() {
return this.uploadRunning;
@ -160,7 +165,7 @@ class watchFolderService {
pushEvent(event: EventQueueItem) {
this.eventQueue.push(event);
debounce(this.runNextEvent.bind(this), 300)();
this.debouncedRunNextEvent();
}
async pushTrashedDir(path: string) {
@ -255,7 +260,7 @@ class watchFolderService {
} else {
await this.processTrashEvent();
this.setIsEventRunning(false);
this.runNextEvent();
setTimeout(() => this.runNextEvent(), 0);
}
} catch (e) {
logError(e, 'runNextEvent failed');

View file

@ -1,7 +1,4 @@
export type DeduplicateContextType = {
clubSameTimeFilesOnly: boolean;
setClubSameTimeFilesOnly: (clubSameTimeFilesOnly: boolean) => void;
fileSizeMap: Map<number, number>;
isOnDeduplicatePage: boolean;
collectionNameMap: Map<number, string>;
};

View file

@ -140,11 +140,12 @@ export function hasMobileSubscription(subscription: Subscription) {
}
export function hasExceededStorageQuota(userDetails: UserDetails) {
const bonusStorage = userDetails.storageBonus ?? 0;
if (isPartOfFamily(userDetails.familyData)) {
const usage = getTotalFamilyUsage(userDetails.familyData);
return usage > userDetails.familyData.storage;
return usage > (userDetails.familyData.storage + bonusStorage);
} else {
return userDetails.usage > userDetails.subscription.storage;
return userDetails.usage > (userDetails.subscription.storage + bonusStorage);
}
}

View file

@ -61,6 +61,13 @@ const DATE_TIME_PARSING_TEST_FILE_NAMES = [
},
];
const DATE_TIME_PARSING_TEST_FILE_NAMES_MUST_FAIL = [
'Snapchat-431959199.mp4.',
'Snapchat-400000000.mp4',
'Snapchat-900000000.mp4',
'Snapchat-100-10-20-19-15-12',
];
const FILE_NAME_TO_JSON_NAME = [
{
filename: 'IMG20210211125718-edited.jpg',
@ -387,6 +394,16 @@ function parseDateTimeFromFileNameTest() {
}
}
);
DATE_TIME_PARSING_TEST_FILE_NAMES_MUST_FAIL.forEach((fileName) => {
const dateTime = tryToParseDateTime(fileName);
if (dateTime) {
throw Error(
`parseDateTimeFromFileNameTest failed ❌ ,
for ${fileName}
expected: null got: ${dateTime}`
);
}
});
console.log('parseDateTimeFromFileNameTest passed ✅');
}

View file

@ -25,8 +25,14 @@ export const sendOtt = (appName: APPS, email: string) => {
});
};
export const verifyOtt = (email: string, ott: string) =>
HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
export const verifyOtt = (email: string, ott: string, referral: string) => {
const cleanedReferral = `web:${referral?.trim() || ''}`;
return HTTPService.post(`${ENDPOINT}/users/verify-email`, {
email,
ott,
source: cleanedReferral,
});
};
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
HTTPService.put(

View file

@ -11,7 +11,10 @@ import {
import { isWeakPassword } from '@ente/accounts/utils';
import { generateKeyAndSRPAttributes } from '@ente/accounts/utils/srp';
import { setJustSignedUp } from '@ente/shared/storage/localStorage/helpers';
import {
setJustSignedUp,
setLocalReferralSource,
} from '@ente/shared/storage/localStorage/helpers';
import { SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
import { PAGES } from '@ente/accounts/constants/pages';
import {
@ -19,8 +22,11 @@ import {
Checkbox,
FormControlLabel,
FormGroup,
IconButton,
InputAdornment,
Link,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
@ -34,11 +40,13 @@ import ShowHidePassword from '@ente/shared/components/Form/ShowHidePassword';
import { APPS } from '@ente/shared/apps/constants';
import { NextRouter } from 'next/router';
import { logError } from '@ente/shared/sentry';
import InfoOutlined from '@mui/icons-material/InfoOutlined';
interface FormValues {
email: string;
passphrase: string;
confirm: string;
referral: string;
}
interface SignUpProps {
@ -63,7 +71,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
};
const registerUser = async (
{ email, passphrase, confirm }: FormValues,
{ email, passphrase, confirm, referral }: FormValues,
{ setFieldError }: FormikHelpers<FormValues>
) => {
try {
@ -74,6 +82,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
setLoading(true);
try {
setData(LS_KEYS.USER, { email });
setLocalReferralSource(referral);
await sendOtt(appName, email);
} catch (e) {
setFieldError('confirm', `${t('UNKNOWN_ERROR')} ${e.message}`);
@ -115,6 +124,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
email: '',
passphrase: '',
confirm: '',
referral: '',
}}
validationSchema={Yup.object().shape({
email: Yup.string()
@ -192,12 +202,47 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
<PasswordStrengthHint
password={values.passphrase}
/>
<Box sx={{ width: '100%' }}>
<Typography
textAlign={'left'}
color="text.secondary"
mt={'24px'}>
{t('REFERRAL_CODE_HINT')}
</Typography>
<TextField
hiddenLabel
InputProps={{
endAdornment: (
<InputAdornment position="end">
<Tooltip
title={t('REFERRAL_INFO')}>
<IconButton
tabIndex={-1}
color="secondary"
edge={'end'}>
<InfoOutlined />
</IconButton>
</Tooltip>
</InputAdornment>
),
}}
fullWidth
name="referral"
type="text"
value={values.referral}
onChange={handleChange('referral')}
error={Boolean(errors.referral)}
disabled={loading}
/>
</Box>
<FormGroup sx={{ width: '100%' }}>
<FormControlLabel
sx={{
color: 'text.muted',
ml: 0,
mt: 2,
mb: 0,
}}
control={
<Checkbox
@ -234,7 +279,7 @@ export default function SignUp({ router, appName, login }: SignUpProps) {
/>
</FormGroup>
</VerticallyCentered>
<Box my={4}>
<Box mb={4}>
<SubmitButton
sx={{ my: 0 }}
buttonText={t('CREATE_ACCOUNT')}

View file

@ -73,7 +73,11 @@ export default function Credentials({
setUser(user);
let key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key && isElectron()) {
try {
key = await ElectronAPIs.getEncryptionKey();
} catch (e) {
logError(e, 'getEncryptionKey failed');
}
if (key) {
await saveKeyInSessionStore(
SESSION_KEYS.ENCRYPTION_KEY,

View file

@ -7,7 +7,10 @@ import { verifyOtt, sendOtt, putAttributes } from '../api/user';
import { logoutUser } from '../services/user';
import { configureSRP } from '../services/srp';
import { clearFiles } from '@ente/shared/storage/localForage/helpers';
import { setIsFirstLogin } from '@ente/shared/storage/localStorage/helpers';
import {
getLocalReferralSource,
setIsFirstLogin,
} from '@ente/shared/storage/localStorage/helpers';
import { clearKeys } from '@ente/shared/storage/sessionStorage';
import { PAGES } from '../constants/pages';
import { KeyAttributes, User } from '@ente/shared/user/types';
@ -58,7 +61,8 @@ export default function VerifyPage({ appContext, router, appName }: PageProps) {
setFieldError
) => {
try {
const resp = await verifyOtt(email, ott);
const referralSource = getLocalReferralSource();
const resp = await verifyOtt(email, ott, referralSource);
const {
keyAttributes,
encryptedToken,

View file

@ -15,8 +15,6 @@ export enum PHOTOS_PAGES {
SHARED_ALBUMS = '/shared-albums',
// ML_DEBUG = '/ml-debug',
DEDUPLICATE = '/deduplicate',
// AUTH page is used to show (auth)enticator codes
AUTH = '/auth',
}
export enum AUTH_PAGES {

View file

@ -8,6 +8,7 @@ import { setRecoveryKey } from '@ente/accounts/api/user';
import { logError } from '@ente/shared/sentry';
import isElectron from 'is-electron';
import ElectronAPIs from '../electron';
import { addLogLine } from '../logging';
const LOGIN_SUB_KEY_LENGTH = 32;
const LOGIN_SUB_KEY_ID = 1;
@ -104,7 +105,7 @@ export const saveKeyInSessionStore = async (
key
);
setKey(keyType, sessionKeyAttributes);
console.log('fromDesktop', fromDesktop);
addLogLine('fromDesktop', fromDesktop);
if (
isElectron() &&
!fromDesktop &&

View file

@ -0,0 +1,84 @@
import * as Comlink from 'comlink';
import { LimitedCache } from '@ente/shared/storage/cacheStorage/types';
import {
ProxiedWorkerLimitedCache,
WorkerSafeElectronClient,
} from './worker/client';
import { wrap } from 'comlink';
import { deserializeToResponse, serializeResponse } from './worker/utils/proxy';
import { runningInWorker } from '@ente/shared/platform';
import { ElectronAPIsType } from './types';
export interface LimitedElectronAPIs
extends Pick<
ElectronAPIsType,
| 'openDiskCache'
| 'deleteDiskCache'
| 'getSentryUserID'
| 'convertToJPEG'
> {}
class WorkerSafeElectronServiceImpl implements LimitedElectronAPIs {
proxiedElectron:
| Comlink.Remote<WorkerSafeElectronClient>
| WorkerSafeElectronClient;
ready: Promise<any>;
constructor() {
this.ready = this.init();
}
private async init() {
if (runningInWorker()) {
const workerSafeElectronClient =
wrap<typeof WorkerSafeElectronClient>(self);
this.proxiedElectron = await new workerSafeElectronClient();
} else {
this.proxiedElectron = new WorkerSafeElectronClient();
}
}
async openDiskCache(cacheName: string) {
await this.ready;
const cache = await this.proxiedElectron.openDiskCache(cacheName);
return {
match: transformMatch(cache.match.bind(cache)),
put: transformPut(cache.put.bind(cache)),
delete: cache.delete.bind(cache),
};
}
async deleteDiskCache(cacheName: string) {
await this.ready;
return await this.proxiedElectron.deleteDiskCache(cacheName);
}
async getSentryUserID() {
await this.ready;
return this.proxiedElectron.getSentryUserID();
}
async convertToJPEG(
inputFileData: Uint8Array,
filename: string
): Promise<Uint8Array> {
await this.ready;
return this.proxiedElectron.convertToJPEG(inputFileData, filename);
}
}
export const WorkerSafeElectronService = new WorkerSafeElectronServiceImpl();
function transformMatch(
fn: ProxiedWorkerLimitedCache['match']
): LimitedCache['match'] {
return async (key: string) => {
return deserializeToResponse(await fn(key));
};
}
function transformPut(
fn: ProxiedWorkerLimitedCache['put']
): LimitedCache['put'] {
return async (key: string, data: Response) => {
fn(key, await serializeResponse(data));
};
}

View file

@ -0,0 +1,61 @@
import * as Comlink from 'comlink';
import { LimitedCache } from '@ente/shared/storage/cacheStorage/types';
import { serializeResponse, deserializeToResponse } from './utils/proxy';
import ElectronAPIs from '@ente/shared/electron';
export interface ProxiedLimitedElectronAPIs {
openDiskCache: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
deleteDiskCache: (cacheName: string) => Promise<boolean>;
getSentryUserID: () => Promise<string>;
convertToJPEG: (
inputFileData: Uint8Array,
filename: string
) => Promise<Uint8Array>;
}
export interface ProxiedWorkerLimitedCache {
match: (key: string) => Promise<ArrayBuffer>;
put: (key: string, data: ArrayBuffer) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}
export class WorkerSafeElectronClient implements ProxiedLimitedElectronAPIs {
async openDiskCache(cacheName: string) {
const cache = await ElectronAPIs.openDiskCache(cacheName);
return Comlink.proxy({
match: Comlink.proxy(transformMatch(cache.match.bind(cache))),
put: Comlink.proxy(transformPut(cache.put.bind(cache))),
delete: Comlink.proxy(cache.delete.bind(cache)),
});
}
async deleteDiskCache(cacheName: string) {
return await ElectronAPIs.deleteDiskCache(cacheName);
}
async getSentryUserID() {
return await ElectronAPIs.getSentryUserID();
}
async convertToJPEG(
inputFileData: Uint8Array,
filename: string
): Promise<Uint8Array> {
return await ElectronAPIs.convertToJPEG(inputFileData, filename);
}
}
function transformMatch(
fn: LimitedCache['match']
): ProxiedWorkerLimitedCache['match'] {
return async (key: string) => {
return serializeResponse(await fn(key));
};
}
function transformPut(
fn: LimitedCache['put']
): ProxiedWorkerLimitedCache['put'] {
return async (key: string, data: ArrayBuffer) => {
fn(key, deserializeToResponse(data));
};
}

View file

@ -86,6 +86,8 @@ export const CustomError = {
ServerError: 'server error',
FILE_NOT_FOUND: 'file not found',
UNSUPPORTED_PLATFORM: 'Unsupported platform',
MODEL_DOWNLOAD_PENDING:
'Model download pending, skipping clip search request',
};
export function handleUploadError(error: any): Error {

View file

@ -1,4 +1,4 @@
import ElectronAPIs from '@ente/shared/electron';
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
import {
getLocalSentryUserID,
setLocalSentryUserID,
@ -12,7 +12,7 @@ import { HttpStatusCode } from 'axios';
export async function getSentryUserID() {
if (isElectron()) {
return await ElectronAPIs.getSentryUserID();
return await WorkerSafeElectronService.getSentryUserID();
} else {
let anonymizeUserID = getLocalSentryUserID();
if (!anonymizeUserID) {

View file

@ -1,28 +1,17 @@
import { LimitedCacheStorage } from './types';
import { runningInElectron, runningInWorker } from '@ente/shared/platform';
import { WorkerElectronCacheStorageService } from './workerElectron/service';
import ElectronAPIs from '@ente/shared/electron';
import { runningInElectron } from '@ente/shared/platform';
import { WorkerSafeElectronService } from '@ente/shared/electron/service';
class cacheStorageFactory {
workerElectronCacheStorageServiceInstance: WorkerElectronCacheStorageService;
getCacheStorage(): LimitedCacheStorage {
if (runningInElectron()) {
if (runningInWorker()) {
if (!this.workerElectronCacheStorageServiceInstance) {
this.workerElectronCacheStorageServiceInstance =
new WorkerElectronCacheStorageService();
}
return this.workerElectronCacheStorageServiceInstance;
} else {
return {
open(cacheName) {
return ElectronAPIs.openDiskCache(cacheName);
return WorkerSafeElectronService.openDiskCache(cacheName);
},
delete(cacheName) {
return ElectronAPIs.deleteDiskCache(cacheName);
return WorkerSafeElectronService.deleteDiskCache(cacheName);
},
};
}
} else {
return transformBrowserCacheStorageToLimitedCacheStorage(caches);
}

View file

@ -8,13 +8,3 @@ export interface LimitedCache {
put: (key: string, data: Response) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}
export interface ProxiedLimitedCacheStorage {
open: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>;
delete: (cacheName: string) => Promise<boolean>;
}
export interface ProxiedWorkerLimitedCache {
match: (key: string) => Promise<ArrayBuffer>;
put: (key: string, data: ArrayBuffer) => Promise<void>;
delete: (key: string) => Promise<boolean>;
}

View file

@ -1,41 +0,0 @@
import * as Comlink from 'comlink';
import {
LimitedCache,
ProxiedLimitedCacheStorage,
ProxiedWorkerLimitedCache,
} from '@ente/shared/storage/cacheStorage/types';
import { serializeResponse, deserializeToResponse } from './utils/proxy';
import ElectronAPIs from '@ente/shared/electron';
export class WorkerElectronCacheStorageClient
implements ProxiedLimitedCacheStorage
{
async open(cacheName: string) {
const cache = await ElectronAPIs.openDiskCache(cacheName);
return Comlink.proxy({
match: Comlink.proxy(transformMatch(cache.match.bind(cache))),
put: Comlink.proxy(transformPut(cache.put.bind(cache))),
delete: Comlink.proxy(cache.delete.bind(cache)),
});
}
async delete(cacheName: string) {
return await ElectronAPIs.deleteDiskCache(cacheName);
}
}
function transformMatch(
fn: LimitedCache['match']
): ProxiedWorkerLimitedCache['match'] {
return async (key: string) => {
return serializeResponse(await fn(key));
};
}
function transformPut(
fn: LimitedCache['put']
): ProxiedWorkerLimitedCache['put'] {
return async (key: string, data: ArrayBuffer) => {
fn(key, deserializeToResponse(data));
};
}

View file

@ -1,55 +0,0 @@
import * as Comlink from 'comlink';
import {
LimitedCache,
LimitedCacheStorage,
ProxiedWorkerLimitedCache,
} from '../types';
import { WorkerElectronCacheStorageClient } from './client';
import { wrap } from 'comlink';
import { deserializeToResponse, serializeResponse } from './utils/proxy';
export class WorkerElectronCacheStorageService implements LimitedCacheStorage {
proxiedElectronCacheService: Comlink.Remote<WorkerElectronCacheStorageClient>;
ready: Promise<any>;
constructor() {
this.ready = this.init();
}
async init() {
const electronCacheStorageProxy =
wrap<typeof WorkerElectronCacheStorageClient>(self);
this.proxiedElectronCacheService =
await new electronCacheStorageProxy();
}
async open(cacheName: string) {
await this.ready;
const cache = await this.proxiedElectronCacheService.open(cacheName);
return {
match: transformMatch(cache.match.bind(cache)),
put: transformPut(cache.put.bind(cache)),
delete: cache.delete.bind(cache),
};
}
async delete(cacheName: string) {
await this.ready;
return await this.proxiedElectronCacheService.delete(cacheName);
}
}
function transformMatch(
fn: ProxiedWorkerLimitedCache['match']
): LimitedCache['match'] {
return async (key: string) => {
return deserializeToResponse(await fn(key));
};
}
function transformPut(
fn: ProxiedWorkerLimitedCache['put']
): LimitedCache['put'] {
return async (key: string, data: Response) => {
fn(key, await serializeResponse(data));
};
}

View file

@ -57,3 +57,11 @@ export function getLocalSentryUserID() {
export function setLocalSentryUserID(id: string) {
setData(LS_KEYS.AnonymizedUserID, { id });
}
export function getLocalReferralSource() {
return getData(LS_KEYS.REFERRAL_SOURCE)?.source;
}
export function setLocalReferralSource(source: string) {
setData(LS_KEYS.REFERRAL_SOURCE, { source });
}

View file

@ -27,6 +27,7 @@ export enum LS_KEYS {
SRP_ATTRIBUTES = 'srpAttributes',
OPT_OUT_OF_CRASH_REPORTS = 'optOutOfCrashReports',
CF_PROXY_DISABLED = 'cfProxyDisabled',
REFERRAL_SOURCE = 'referralSource',
}
export const setData = (key: LS_KEYS, value: object) => {

View file

@ -14,6 +14,8 @@ interface DateComponent<T = number> {
second: T;
}
const currentYear = new Date().getFullYear();
export function getUnixTimeInMicroSecondsWithDelta(delta: TimeDelta): number {
let currentDate = new Date();
if (delta?.hours) {
@ -112,7 +114,8 @@ function getDateComponentsFromSymbolJoinedString(
}
function validateAndGetDateFromComponents(
dateComponent: DateComponent<number>
dateComponent: DateComponent<number>,
options = { minYear: 1990, maxYear: currentYear + 1 }
) {
let date = getDateFromComponents(dateComponent);
if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) {
@ -123,6 +126,12 @@ function validateAndGetDateFromComponents(
if (!isDatePartValid(date, dateComponent)) {
return null;
}
if (
date.getFullYear() < options.minYear ||
date.getFullYear() > options.maxYear
) {
return null;
}
return date;
}

View file

@ -1,5 +1,5 @@
import { expose, Remote, wrap } from 'comlink';
import { WorkerElectronCacheStorageClient } from '@ente/shared/storage/cacheStorage/workerElectron/client';
import { WorkerSafeElectronClient } from '@ente/shared/electron/worker/client';
import { addLocalLog } from '@ente/shared/logging';
export class ComlinkWorker<T extends new () => InstanceType<T>> {
@ -17,7 +17,7 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> {
addLocalLog(() => `Initiated ${this.name}`);
const comlink = wrap<T>(this.worker);
this.remote = new comlink() as Promise<Remote<InstanceType<T>>>;
expose(WorkerElectronCacheStorageClient, this.worker);
expose(WorkerSafeElectronClient, this.worker);
}
public getName() {

View file

@ -744,11 +744,6 @@
dependencies:
base-x "^3.0.6"
"@types/debounce-promise@^3.1.3":
version "3.1.4"
resolved "https://registry.npmjs.org/@types/debounce-promise/-/debounce-promise-3.1.4.tgz"
integrity sha512-9SEVY3nsz+uMN2DwDocftB5TAgZe7D0cOzxxRhpotWs6T4QFqRaTXpXbOSzbk31/7iYcfCkJJPwWGzTxyuGhCg==
"@types/estree@*", "@types/estree@^1.0.0":
version "1.0.1"
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz"
@ -1622,10 +1617,10 @@ dayjs@^1.10.0:
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.6.tgz"
integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw==
debounce-promise@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz"
integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==
debounce@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/debounce/-/debounce-2.0.0.tgz#b2f914518a1481466f4edaee0b063e4d473ad549"
integrity sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==
debug@4, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4:
version "4.3.4"
@ -2523,21 +2518,21 @@ hdbscan@0.0.1-alpha.5:
dependencies:
kd-tree-javascript "^1.0.3"
heic-convert@^1.2.4:
version "1.2.4"
resolved "https://registry.npmjs.org/heic-convert/-/heic-convert-1.2.4.tgz"
integrity sha512-klJHyv+BqbgKiCQvCqI9IKIvweCcohDuDl0Jphearj8+16+v8eff2piVevHqq4dW9TK0r1onTR6PKHP1I4hdbA==
heic-convert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/heic-convert/-/heic-convert-2.0.0.tgz#8250d56247310eb1121ef791a4b9ecf2cc0cc2a0"
integrity sha512-Kk2Ue5D7sAhyCmnHPLXQ4tzYH1qINmgppgqFn66x+DmClyL6sdmqGbHJ9cETy0Pxz5Ixz5w5JWJuIv9QqC1oKg==
dependencies:
heic-decode "^1.1.2"
jpeg-js "^0.4.1"
pngjs "^3.4.0"
heic-decode "^2.0.0"
jpeg-js "^0.4.4"
pngjs "^6.0.0"
heic-decode@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/heic-decode/-/heic-decode-1.1.2.tgz"
integrity sha512-UF8teegxvzQPdSTcx5frIUhitNDliz/9Pui0JFdIqVRE00spVE33DcCYtZqaLNyd4y5RP/QQWZFIc1YWVKKm2A==
heic-decode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/heic-decode/-/heic-decode-2.0.0.tgz#77ab96ee1a255f0a9952d0e88d584bca30577114"
integrity sha512-NU+zsiDvdL+EebyTjrEqjkO2XYI7FgLhQzsbmO8dnnYce3S0PBSDm/ZyI4KpcGPXYEdb5W72vp/AQFuc4F8ASg==
dependencies:
libheif-js "^1.10.0"
libheif-js "^1.17.1"
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
@ -2904,9 +2899,9 @@ isexe@^2.0.0:
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
jpeg-js@^0.4.1:
jpeg-js@^0.4.4:
version "0.4.4"
resolved "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz"
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
js-sdsl@^4.1.4:
@ -3006,10 +3001,10 @@ levn@^0.4.1:
prelude-ls "^1.2.1"
type-check "~0.4.0"
libheif-js@^1.10.0:
version "1.12.0"
resolved "https://registry.npmjs.org/libheif-js/-/libheif-js-1.12.0.tgz"
integrity sha512-hDs6xQ7028VOwAFwEtM0Q+B2x2NW69Jb2MhQFUbk3rUrHzz4qo5mqS8VrqNgYnSc8TiUGnR691LnO4uIfEE23w==
libheif-js@^1.17.1:
version "1.17.1"
resolved "https://registry.yarnpkg.com/libheif-js/-/libheif-js-1.17.1.tgz#7772cc5a31098df0354f0fadb49a939030765acd"
integrity sha512-g9wBm/CasGZMjmH3B2sD9+AO7Y5+79F0oPS+sdAulSxQeYeCeiTIP+lDqvlPofD+y76wvfVtotKZ8AuvZQnWgg==
libsodium-wrappers@^0.7.8:
version "0.7.9"
@ -3484,6 +3479,11 @@ otpauth@^9.0.2:
dependencies:
jssha "~3.3.0"
p-debounce@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/p-debounce/-/p-debounce-4.0.0.tgz#348e3f44489baa9435cc7d807f17b3bb2fb16b24"
integrity sha512-4Ispi9I9qYGO4lueiLDhe4q4iK5ERK8reLsuzH6BPaXn53EGaua8H66PXIFGrW897hwjXp+pVLrm/DLxN0RF0A==
p-limit@^3.0.2:
version "3.1.0"
resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz"
@ -3609,10 +3609,10 @@ piexifjs@^1.0.6:
resolved "https://registry.npmjs.org/piexifjs/-/piexifjs-1.0.6.tgz"
integrity sha512-0wVyH0cKohzBQ5Gi2V1BuxYpxWfxF3cSqfFXfPIpl5tl9XLS5z4ogqhUCD20AbHi0h9aJkqXNJnkVev6gwh2ag==
pngjs@^3.4.0:
version "3.4.0"
resolved "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
pngjs@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-6.0.0.tgz#ca9e5d2aa48db0228a52c419c3308e87720da821"
integrity sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==
postcss@8.4.14:
version "8.4.14"