Merge branch 'master' into handle_subscription_errors

This commit is contained in:
Abhinav-grd 2021-02-09 12:26:42 +05:30
commit 49d981e86a
12 changed files with 299 additions and 141 deletions

1
.gitignore vendored
View file

@ -4,7 +4,6 @@
/node_modules
/.pnp
.pnp.js
package-lock.json
# testing
/coverage

View file

@ -24,6 +24,7 @@
"react-dom": "16.13.1",
"react-dropzone": "^11.2.4",
"react-photoswipe": "^1.3.0",
"react-top-loading-bar": "^2.0.1",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6",
"react-window-infinite-loader": "^1.0.5",
@ -44,7 +45,7 @@
"@types/yup": "^0.29.7",
"babel-plugin-styled-components": "^1.11.1",
"next-on-netlify": "^2.4.0",
"typescript": "^4.0.2",
"typescript": "^4.1.3",
"worker-plugin": "^5.0.0"
},
"standard": {

BIN
public/fav-button.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

View file

@ -161,7 +161,6 @@ export default function App({ Component, pageProps }) {
<FullScreenDropZone
closeModal={closeUploadModal}
showModal={showUploadModal}
uploadModalView={uploadModalView}
>
<Head>
<title>ente.io | Privacy friendly alternative to Google Photos</title>

View file

@ -16,11 +16,11 @@ export default function CreateCollection(props) {
if (acceptedFiles == null)
return;
let commonPathPrefix: string = (() => {
const A: string[] = acceptedFiles.map(files => files.path);
let a1 = A[0], a2 = A[A.length - 1], L = a1.length, i = 0;
while (i < L && a1.charAt(i) === a2.charAt(i)) i++;
return a1.substring(0, i);
const paths: string[] = acceptedFiles.map(files => files.path);
paths.sort();
let firstPath = paths[0], lastPath = paths[paths.length - 1], L = firstPath.length, i = 0;
while (i < L && firstPath.charAt(i) === lastPath.charAt(i)) i++;
return firstPath.substring(0, i);
})();
if (commonPathPrefix)
commonPathPrefix = commonPathPrefix.substr(1, commonPathPrefix.lastIndexOf('/') - 1);

View file

@ -13,10 +13,10 @@ interface IProps {
const Cont = styled.div<{ disabled: boolean }>`
background: #555 url(/image.svg) no-repeat center;
margin: 0 4px;
display: inline-block;
width: 192px;
display: block;
width: fit-content;
height: 192px;
min-width: 100%;
overflow: hidden;
position: relative;
cursor: ${props => props.disabled ? 'not-allowed' : 'pointer'};

View file

@ -7,22 +7,34 @@ import {
getFile,
getPreview,
fetchData,
localFiles,
} from 'services/fileService';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import PreviewCard from './components/PreviewCard';
import { getActualKey } from 'utils/common/key';
import { getActualKey, getToken } from 'utils/common/key';
import styled from 'styled-components';
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
import { Options } from 'photoswipe';
import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeList as List } from 'react-window';
import LoadingBar from 'react-top-loading-bar';
import Collections from './components/Collections';
import SadFace from 'components/SadFace';
import Upload from './components/Upload';
import { collection, fetchCollections, collectionLatestFile, getCollectionLatestFile, getFavItemIds } from 'services/collectionService';
import {
collection,
fetchUpdatedCollections,
collectionLatestFile,
getCollectionLatestFile,
getFavItemIds,
getLocalCollections,
} from 'services/collectionService';
import constants from 'utils/strings/constants';
import { ErrorAlert } from './components/ErrorAlert';
const DATE_CONTAINER_HEIGHT = 45;
const IMAGE_CONTAINER_HEIGHT = 200;
const NO_OF_PAGES = 2;
enum ITEM_TYPE {
TIME = 'TIME',
TILE = 'TILE',
@ -30,7 +42,7 @@ enum ITEM_TYPE {
export enum FILE_TYPE {
IMAGE,
VIDEO,
OTHERS
OTHERS,
}
interface TimeStampListItem {
@ -68,8 +80,11 @@ const DeadCenter = styled.div`
flex-direction: column;
`;
const ListContainer = styled.div`
display: flex;
const ListContainer = styled.div<{ columns: number }>`
display: grid;
grid-template-columns: repeat(${(props) => props.columns}, 1fr);
grid-column-gap: 8px;
padding: 0 8px;
max-width: 100%;
color: #fff;
@ -87,13 +102,12 @@ const ListContainer = styled.div`
`;
const DateContainer = styled.div`
padding: 0 4px;
padding-top: 15px;
`;
export default function Gallery(props) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [reload, setReload] = useState(0);
const [collections, setCollections] = useState<collection[]>([]);
const [collectionLatestFile, setCollectionLatestFile] = useState<
collectionLatestFile[]
@ -108,37 +122,53 @@ export default function Gallery(props) {
const fetching: { [k: number]: boolean } = {};
const [errorCode, setErrorCode] = useState<number>(null);
const [sinceTime, setSinceTime] = useState(0);
const [progress, setProgress] = useState(0);
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
const token = getData(LS_KEYS.USER).token;
if (!key) {
router.push('/');
}
const main = async () => {
setLoading(true);
const encryptionKey = await getActualKey();
const collections = await fetchCollections(token, encryptionKey);
const data = await fetchData(token, collections);
const collectionLatestFile = await getCollectionLatestFile(collections, token);
const favItemIds = await getFavItemIds(data);
setCollections(collections);
const data = await localFiles();
const collections = await getLocalCollections();
setData(data);
setCollectionLatestFile(collectionLatestFile);
setFavItemIds(favItemIds);
setCollections(collections);
setLoading(false);
setProgress(80);
await syncWithRemote();
setProgress(100);
};
main();
props.setUploadButtonView(true);
}, [reload]);
}, []);
if (!data || loading) {
return (
<div className='text-center'>
<Spinner animation='border' variant='primary' />
</div>
const syncWithRemote = async () => {
const token = getToken();
const encryptionKey = await getActualKey();
const updatedCollections = await fetchUpdatedCollections(
token,
encryptionKey
);
}
const data = await fetchData(token, updatedCollections);
const collections = await getLocalCollections();
const collectionLatestFile = await getCollectionLatestFile(
collections,
data
);
const favItemIds = await getFavItemIds(data);
if (updatedCollections.length > 0) {
setCollections(collections);
setData(data);
}
setCollectionLatestFile(collectionLatestFile);
setFavItemIds(favItemIds);
setSinceTime(new Date().getTime());
props.setUploadButtonView(true);
};
const updateUrl = (index: number) => (url: string) => {
data[index] = {
@ -147,7 +177,10 @@ export default function Gallery(props) {
w: window.innerWidth,
h: window.innerHeight,
};
if (data[index].metadata.fileType === FILE_TYPE.VIDEO && !data[index].html) {
if (
data[index].metadata.fileType === FILE_TYPE.VIDEO &&
!data[index].html
) {
data[index].html = `
<div class="video-loading">
<img src="${url}" />
@ -158,7 +191,10 @@ export default function Gallery(props) {
`;
delete data[index].src;
}
if (data[index].metadata.fileType === FILE_TYPE.IMAGE && !data[index].src) {
if (
data[index].metadata.fileType === FILE_TYPE.IMAGE &&
!data[index].src
) {
data[index].src = url;
}
setData(data);
@ -185,7 +221,7 @@ export default function Gallery(props) {
const handleClose = () => {
setOpen(false);
// setReload(Math.random());
// syncWithRemote();
};
const onThumbnailClick = (index: number) => () => {
@ -225,7 +261,10 @@ export default function Gallery(props) {
// ignore
}
}
if ((!item.src || item.src === item.msrc) && !fetching[item.dataIndex]) {
if (
(!item.src || item.src === item.msrc) &&
!fetching[item.dataIndex]
) {
fetching[item.dataIndex] = true;
const url = await getFile(token, item);
updateSrcUrl(item.dataIndex, url);
@ -251,6 +290,14 @@ export default function Gallery(props) {
}
};
if (!data || loading) {
return (
<div className="text-center">
<Spinner animation="border" variant="primary" />
</div>
);
}
const selectCollection = (id?: string) => {
const href = `/gallery?collection=${id || ''}`;
router.push(href, undefined, { shallow: true });
@ -287,6 +334,11 @@ export default function Gallery(props) {
return (
<>
<ErrorAlert errorCode={errorCode} />
<LoadingBar
color="#007bff"
progress={progress}
onLoaderFinished={() => setProgress(0)}
/>
<Collections
collections={collections}
selected={router.query.collection?.toString()}
@ -297,9 +349,8 @@ export default function Gallery(props) {
closeUploadModal={props.closeUploadModal}
showUploadModal={props.showUploadModal}
collectionLatestFile={collectionLatestFile}
refetchData={() => setReload(Math.random())}
refetchData={syncWithRemote}
setErrorCode={setErrorCode}
/>
{filteredData.length ? (
<Container>
@ -322,20 +373,28 @@ export default function Gallery(props) {
filteredData.forEach((item, index) => {
if (
!isSameDay(
new Date(item.metadata.creationTime / 1000),
new Date(
item.metadata.creationTime / 1000
),
new Date(currentDate)
)
) {
currentDate = item.metadata.creationTime / 1000;
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
weekday: 'short',
year: 'numeric',
month: 'long',
day: 'numeric',
});
currentDate =
item.metadata.creationTime / 1000;
const dateTimeFormat = new Intl.DateTimeFormat(
'en-IN',
{
weekday: 'short',
year: 'numeric',
month: 'long',
day: 'numeric',
}
);
timeStampList.push({
itemType: ITEM_TYPE.TIME,
date: dateTimeFormat.format(currentDate),
date: dateTimeFormat.format(
currentDate
),
});
timeStampList.push({
itemType: ITEM_TYPE.TILE,
@ -345,7 +404,9 @@ export default function Gallery(props) {
listItemIndex = 1;
} else {
if (listItemIndex < columns) {
timeStampList[timeStampList.length - 1].items.push(item);
timeStampList[
timeStampList.length - 1
].items.push(item);
listItemIndex++;
} else {
listItemIndex = 1;
@ -357,36 +418,61 @@ export default function Gallery(props) {
}
}
});
const extraRowsToRender = Math.ceil(
(NO_OF_PAGES * height) / IMAGE_CONTAINER_HEIGHT
);
return (
<List
itemSize={(index) =>
timeStampList[index].itemType === ITEM_TYPE.TIME
? 30
: 200
timeStampList[index].itemType ===
ITEM_TYPE.TIME
? DATE_CONTAINER_HEIGHT
: IMAGE_CONTAINER_HEIGHT
}
height={height}
width={width}
itemCount={timeStampList.length}
key={`${router.query.collection}-${columns}`}
key={`${router.query.collection}-${columns}-${sinceTime}`}
overscanCount={extraRowsToRender}
>
{({ index, style }) => {
return (
<ListItem style={style}>
<ListContainer>
{timeStampList[index].itemType ===
ITEM_TYPE.TIME ? (
<DateContainer>
{timeStampList[index].date}
</DateContainer>
) : (
timeStampList[index].items.map((item, idx) => {
<ListContainer
columns={
timeStampList[index]
.itemType ===
ITEM_TYPE.TIME
? 1
: columns
}
>
{timeStampList[index]
.itemType ===
ITEM_TYPE.TIME ? (
<DateContainer>
{
timeStampList[
index
].date
}
</DateContainer>
) : (
timeStampList[
index
].items.map(
(item, idx) => {
return getThumbnail(
filteredData,
timeStampList[index].itemStartIndex + idx
timeStampList[
index
]
.itemStartIndex +
idx
);
})
)}
}
)
)}
</ListContainer>
</ListItem>
);
@ -406,10 +492,10 @@ export default function Gallery(props) {
/>
</Container>
) : (
<DeadCenter>
<div>{constants.NOTHING_HERE}</div>
</DeadCenter>
)}
<DeadCenter>
<div>{constants.NOTHING_HERE}</div>
</DeadCenter>
)}
</>
);
}

View file

@ -97,32 +97,54 @@ const getCollections = async (
return await Promise.all(promises);
}
catch (e) {
console.log("getCollections falied- " + e);
console.log("getCollections failed- " + e);
}
};
export const fetchCollections = async (token: string, key: string) => {
const collections = await getCollections(token, '0', key);
const favCollection = collections.filter(collection => collection.type === CollectionType.favorites);
await localForage.setItem('fav-collection', favCollection);
export const getLocalCollections = async (): Promise<collection[]> => {
const collections = await localForage.getItem('collections') as collection[] ?? [];
return collections;
}
export const fetchUpdatedCollections = async (token: string, key: string) => {
const collectionUpdateTime = await localForage.getItem('collection-update-time') as string;
const updatedCollections = await getCollections(token, collectionUpdateTime ?? '0', key) || [];
const favCollection = await localForage.getItem('fav-collection') as collection[] ?? updatedCollections.filter(collection => collection.type === CollectionType.favorites);
const localCollections = await getLocalCollections();
const allCollectionsInstances = [...localCollections, ...updatedCollections];
var latestCollectionsInstances = new Map<string, collection>();
allCollectionsInstances.forEach((collection) => {
if (!latestCollectionsInstances.has(collection.id) || latestCollectionsInstances.get(collection.id).updationTime < collection.updationTime) {
latestCollectionsInstances.set(collection.id, collection);
}
});
let collections = [];
for (const [_, collection] of latestCollectionsInstances) {
collections.push(collection);
}
await localForage.setItem('fav-collection', favCollection);
await localForage.setItem('collections', collections);
return updatedCollections;
};
export const getCollectionLatestFile = async (
export const getCollectionLatestFile = (
collections: collection[],
token
): Promise<collectionLatestFile[]> => {
return Promise.all(
collections.map(async collection => {
const sinceTime: string = (Number(await localForage.getItem<string>(`${collection.id}-time`)) - 1).toString();
const files: file[] = await getFiles([collection], sinceTime, "1", token) || [];
files: file[]
): collectionLatestFile[] => {
const latestFile = new Map<number, file>();
const collectionMap = new Map<number, collection>();
return {
file: files[0],
collection,
}
}))
};
collections.forEach(collection => collectionMap.set(Number(collection.id), collection));
files.forEach(file => {
if (!latestFile.has(file.collectionID)) {
latestFile.set(file.collectionID, file)
}
});
let allCollectionLatestFile: collectionLatestFile[] = [];
for (const [collectionID, file] of latestFile) {
allCollectionLatestFile.push({ collection: collectionMap.get(collectionID), file });
}
return allCollectionLatestFile;
}
export const getFavItemIds = async (files: file[]): Promise<Set<number>> => {

View file

@ -66,13 +66,23 @@ export const fetchData = async (token, collections) => {
);
}
export const localFiles = async () => {
let files: Array<file> = (await localForage.getItem<file[]>('files')) || [];
return files;
}
export const fetchFiles = async (
token: string,
collections: collection[]
) => {
let files: Array<file> = (await localForage.getItem<file[]>('files')) || [];
const fetchedFiles = await getFiles(collections, null, "100", token);
let files = await localFiles();
const collectionUpdationTime = new Map<string, string>();
let fetchedFiles = [];
for (let collection of collections) {
const files = await getFiles(collection, null, 100, token);
fetchedFiles.push(...files);
collectionUpdationTime.set(collection.id, files.length > 0 ? files.slice(-1)[0].updationTime.toString() : "0");
}
files.push(...fetchedFiles);
var latestFiles = new Map<string, file>();
files.forEach((file) => {
@ -82,7 +92,7 @@ export const fetchFiles = async (
}
});
files = [];
for (const [_, file] of latestFiles.entries()) {
for (const [_, file] of latestFiles) {
if (!file.isDeleted)
files.push(file);
}
@ -90,53 +100,57 @@ export const fetchFiles = async (
(a, b) => b.metadata.creationTime - a.metadata.creationTime
);
await localForage.setItem('files', files);
for (let [collectionID, updationTime] of collectionUpdationTime) {
await localForage.setItem(`${collectionID}-time`, updationTime);
}
let updationTime = await localForage.getItem('collection-update-time') as number;
for (let collection of collections) {
updationTime = Math.max(updationTime, collection.updationTime);
}
await localForage.setItem('collection-update-time', updationTime);
return files;
};
export const getFiles = async (collections: collection[], sinceTime: string, limit: string, token: string): Promise<file[]> => {
export const getFiles = async (collection: collection, sinceTime: string, limit: number, token: string): Promise<file[]> => {
try {
const worker = await new CryptoWorker();
let promises: Promise<file>[] = [];
for (const index in collections) {
const collection = collections[index];
if (collection.isDeleted) {
// TODO: Remove files in this collection from localForage and cache
continue;
}
let time =
sinceTime || (await localForage.getItem<string>(`${collection.id}-time`)) || "0";
let resp;
do {
resp = await HTTPService.get(`${ENDPOINT}/collections/diff`, {
collectionID: collection.id,
sinceTime: time,
limit,
},
{
'X-Auth-Token': token
});
promises.push(...resp.data.diff.map(
async (file: file) => {
if (!file.isDeleted) {
file.key = await worker.decryptB64(
file.encryptedKey,
file.keyDecryptionNonce,
collection.key
);
file.metadata = await worker.decryptMetadata(file);
}
return file;
}
));
if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime.toString();
}
} while (resp.data.diff.length);
await localForage.setItem(`${collection.id}-time`, time);
if (collection.isDeleted) {
// TODO: Remove files in this collection from localForage and cache
return;
}
return Promise.all(promises);
let time =
sinceTime || (await localForage.getItem<string>(`${collection.id}-time`)) || "0";
let resp;
do {
resp = await HTTPService.get(`${ENDPOINT}/collections/diff`, {
collectionID: collection.id,
sinceTime: time,
limit: limit.toString(),
},
{
'X-Auth-Token': token
});
promises.push(...resp.data.diff.map(
async (file: file) => {
if (!file.isDeleted) {
file.key = await worker.decryptB64(
file.encryptedKey,
file.keyDecryptionNonce,
collection.key
);
file.metadata = await worker.decryptMetadata(file);
}
return file;
}
));
if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime.toString();
}
} while (resp.data.diff.length === limit);
return await Promise.all(promises);
} catch (e) {
console.log("Get files failed" + e);
}

View file

@ -16,5 +16,5 @@ export const getActualKey = async () => {
}
export const getToken = () => {
return getData(LS_KEYS.USER).token;
return getData(LS_KEYS.USER)?.token;
}

View file

@ -1673,6 +1673,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
attr-accept@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
axios@^0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd"
@ -2848,6 +2853,11 @@ execa@^4.0.2:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
exif-js@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/exif-js/-/exif-js-2.3.0.tgz#9d10819bf571f873813e7640241255ab9ce1a814"
integrity sha1-nRCBm/Vx+HOBPnZAJBJVq5zhqBQ=
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@ -2948,6 +2958,13 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
file-selector@^0.2.2:
version "0.2.4"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==
dependencies:
tslib "^2.0.3"
file-uri-to-path@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@ -4923,6 +4940,15 @@ react-dom@16.13.1:
prop-types "^15.6.2"
scheduler "^0.19.1"
react-dropzone@^11.2.4:
version "11.3.0"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.3.0.tgz#516561c5003e0c0f7d63bd5621f410b1b3496ab3"
integrity sha512-5ffIOi5Uf1X52m4fN8QdcRuAX88nQPfmx6HTTIfF9I3W9Ss1SvRDl/ruZmFf53K7+g3TSaIgVw6a9EK7XoDwHw==
dependencies:
attr-accept "^2.2.1"
file-selector "^0.2.2"
prop-types "^15.7.2"
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
@ -4967,6 +4993,11 @@ react-refresh@0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-top-loading-bar@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/react-top-loading-bar/-/react-top-loading-bar-2.0.1.tgz#c8805ad9c1068766fdd3cadd414e67cfdf1878e9"
integrity sha512-wkRlK9Rte4TU817GDcjlsCoDOxrrnvsNvK609FKyio0EIrmmqjQDz5DB8HbN88CHNZBy5Lh/OBALc03ioWFPuQ==
react-transition-group@^4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
@ -5851,6 +5882,11 @@ tslib@^1.10.0, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
tslib@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a"
integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A==
tty-browserify@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@ -5884,10 +5920,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typescript@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2"
integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ==
typescript@^4.1.3:
version "4.1.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.3.tgz#519d582bd94cba0cf8934c7d8e8467e473f53bb7"
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
uncontrollable@^7.0.0:
version "7.1.1"