Merge branch 'main' into better-comlink-types

This commit is contained in:
Abhinav 2023-02-10 15:10:45 +05:30
commit 2611289052
52 changed files with 1290 additions and 801 deletions

38
.github/workflows/cla.yaml vendored Normal file
View file

@ -0,0 +1,38 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened, closed, synchronize]
jobs:
CLAAssistant:
# This job only runs for pull request comments
if: ${{ github.event.issue.pull_request }}
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: contributor-assistant/github-action@v2.2.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# the below token should have repo scope and must be manually added by you in the repository's secret
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
with:
path-to-signatures: "signatures/version1/cla.json"
path-to-document: "https://github.com/ente-io/cla/blob/main/CLA.md" # e.g. a CLA or a DCO document
# branch should not be protected
branch: "main"
allowlist: enteio
# the followings are the optional inputs - If the optional inputs are not given, then default values will be taken
#remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository)
remote-repository-name: cla
#create-file-commit-message: 'For example: Creating file for storing CLA Signatures'
#signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo'
#custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign'
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
#use-dco-flag: true - If you are using DCO instead of CLA

98
CLA.md Normal file
View file

@ -0,0 +1,98 @@
## Contributor License Agreement
Thank you for your contribution to ente projects.
This contributor license agreement documents the rights granted by contributors
to Ente Technologies, Inc ("ente"). This license is for your protection as a
Contributor as well as the protection of ente, its users, and its licensees; you
may still license your own Contributions under other terms.
In exchange for the ability to participate in the ente community and for other
good consideration, the receipt of which is hereby acknowledged, you accept and
agree to the following terms and conditions for Your present and future
Contributions submitted to ente. Except for the license granted herein to ente
and recipients of software distributed by ente, You reserve all right, title,
and interest in and to Your Contributions.
1. Definitions.
"You" (or "Your") shall mean the copyright owner or legal entity authorized
by the copyright owner that is making this Agreement with ente. For legal
entities, the entity making a Contribution and all other entities that
control, are controlled by, or are under common control with that entity are
considered to be a single Contributor. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the direction or
management of such entity, whether by contract or otherwise, or (ii)
ownership of fifty percent (50%) or more of the outstanding shares, or (iii)
beneficial ownership of such entity.
"Contribution" shall mean any original work of authorship or invention,
including any modifications or additions to an existing work, that is
intentionally submitted by You to ente for inclusion in, or documentation of,
any of the products owned or managed by ente (the "Work"). For the purposes
of this definition, "submitted" means any form of electronic, verbal, or
written communication sent to ente or its representatives, including but not
limited to communication on electronic mailing lists, source code control
systems, and issue tracking systems that are managed by, or on behalf of,
ente for the purpose of discussing and improving the Work, but excluding
communication that is conspicuously marked or otherwise designated in writing
by You as "Not a Contribution."
2. Grant of Copyright License. Subject to the terms and conditions of this
Agreement, You hereby grant to ente and to recipients of software distributed
by ente a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare derivative works of,
publicly display, publicly perform, and distribute Your Contributions and
such derivative works.
3. Grant of Patent License. Subject to the terms and conditions of this
Agreement, You hereby grant to ente and to recipients of software distributed
by ente a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable by You that
are necessarily infringed by Your Contribution(s) alone or by combination of
Your Contribution(s) with the Work to which such Contribution(s) was
submitted. If any entity institutes patent litigation against You or any
other entity (including a cross-claim or counterclaim in a lawsuit) alleging
that your Contribution, or the Work to which you have contributed,
constitutes direct or contributory patent infringement, then any patent
licenses granted to that entity under this Agreement for that Contribution or
Work shall terminate as of the date such litigation is filed.
4. You represent that you are legally entitled to grant the above license. If
your employer(s) has rights to intellectual property that you create that
includes your Contributions, you represent that you have received permission
to make Contributions on behalf of that employer, that your employer has
waived such rights for your contributions to ente, or that your employer has
executed with ente a separate contributor license agreement substantially
similar to this Agreement. If You are a current employee or contractor of
ente, then the terms of your existing Employment Agreement or Consulting
Services Agreement shall supersede this CLA, and remain in full effect.
5. You represent that each of Your Contributions is Your original creation (see
section 7 for submissions on behalf of others). You represent that Your
Contribution submissions include complete details of any third-party license
or other restriction (including, but not limited to, related patents and
trademarks) of which you are personally aware and which are associated with
any part of Your Contributions.
6. You are not expected to provide support for Your Contributions, except to the
extent You desire to provide support. You may provide support for free, for
a fee, or not at all. Unless required by applicable law or agreed to in
writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including,
without limitation, any warranties or conditions of title, non-infringement,
merchantability, or fitness for a particular purpose.
7. Should You wish to submit work that is not Your original creation, You may
submit it to ente separately from any Contribution, identifying the complete
details of its source and of any license or other restriction (including, but
not limited to, related patents, trademarks, and license agreements) of which
you are personally aware, and conspicuously marking the work as "Not a
Contribution". Third-party materials licensed pursuant to: [license name(s)
here]" (substituting the bracketed text with the appropriate license
name(s)).
8. You agree to notify ente of any facts or circumstances of which you become
aware that would make these representations inaccurate in any respect.

View file

@ -62,8 +62,15 @@ We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io]
If you like this project, please consider upgrading to a paid subscription. If you like this project, please consider upgrading to a paid subscription.
If you would like to motivate us to keep building, you can do so by And [star this repo](https://github.com/ente-io/photos-web/stargazers)!
[starring](https://github.com/ente-io/photos-web/stargazers) this project.
<br/>
## 🌍 Translate
Create a copy of
[src/utils/strings/englishConstants.tsx](src/utils/strings/englishConstants.tsx)
and open a PR.
<br/> <br/>

View file

@ -34,7 +34,7 @@
"formik": "^2.1.5", "formik": "^2.1.5",
"heic-convert": "^1.2.4", "heic-convert": "^1.2.4",
"is-electron": "^2.2.0", "is-electron": "^2.2.0",
"jszip": "3.7.1", "jszip": "3.8.0",
"libsodium-wrappers": "^0.7.8", "libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"next": "^13.1.2", "next": "^13.1.2",

View file

@ -10,9 +10,13 @@ interface Iprops {
action: CollectionActions, action: CollectionActions,
loader?: boolean loader?: boolean
) => (...args: any[]) => Promise<void>; ) => (...args: any[]) => Promise<void>;
downloadOptionText?: string;
} }
export function FavoritiesCollectionOption({ handleCollectionAction }: Iprops) { export function OnlyDownloadCollectionOption({
handleCollectionAction,
downloadOptionText = constants.DOWNLOAD,
}: Iprops) {
return ( return (
<OverflowMenuOption <OverflowMenuOption
startIcon={<FileDownloadOutlinedIcon />} startIcon={<FileDownloadOutlinedIcon />}
@ -20,7 +24,7 @@ export function FavoritiesCollectionOption({ handleCollectionAction }: Iprops) {
CollectionActions.CONFIRM_DOWNLOAD, CollectionActions.CONFIRM_DOWNLOAD,
false false
)}> )}>
{constants.DOWNLOAD_COLLECTION} {downloadOptionText}
</OverflowMenuOption> </OverflowMenuOption>
); );
} }

View file

@ -25,6 +25,7 @@ export function QuickOptions({
{!( {!(
collectionSummaryType === CollectionSummaryType.trash || collectionSummaryType === CollectionSummaryType.trash ||
collectionSummaryType === CollectionSummaryType.favorites || collectionSummaryType === CollectionSummaryType.favorites ||
collectionSummaryType === CollectionSummaryType.uncategorized ||
collectionSummaryType === CollectionSummaryType.incomingShare collectionSummaryType === CollectionSummaryType.incomingShare
) && ( ) && (
<Tooltip <Tooltip
@ -51,7 +52,10 @@ export function QuickOptions({
title={ title={
collectionSummaryType === collectionSummaryType ===
CollectionSummaryType.favorites CollectionSummaryType.favorites
? constants.DOWNLOAD_FAVOURITES ? constants.DOWNLOAD_FAVORITES
: collectionSummaryType ===
CollectionSummaryType.uncategorized
? constants.DOWNLOAD_UNCATEGORIZED
: constants.DOWNLOAD_COLLECTION : constants.DOWNLOAD_COLLECTION
}> }>
<IconButton <IconButton

View file

@ -18,7 +18,7 @@ import OverflowMenu from 'components/OverflowMenu/menu';
import { CollectionSummaryType } from 'constants/collection'; import { CollectionSummaryType } from 'constants/collection';
import { TrashCollectionOption } from './TrashCollectionOption'; import { TrashCollectionOption } from './TrashCollectionOption';
import { SharedCollectionOption } from './SharedCollectionOption'; import { SharedCollectionOption } from './SharedCollectionOption';
import { FavoritiesCollectionOption } from './FavoritiesCollectionOption'; import { OnlyDownloadCollectionOption } from './OnlyDownloadCollectionOption';
import { QuickOptions } from './QuickOptions'; import { QuickOptions } from './QuickOptions';
import MoreHoriz from '@mui/icons-material/MoreHoriz'; import MoreHoriz from '@mui/icons-material/MoreHoriz';
import { HorizontalFlex } from 'components/Container'; import { HorizontalFlex } from 'components/Container';
@ -39,7 +39,8 @@ export enum CollectionActions {
ARCHIVE, ARCHIVE,
UNARCHIVE, UNARCHIVE,
CONFIRM_DELETE, CONFIRM_DELETE,
DELETE, DELETE_WITH_FILES,
DELETE_BUT_KEEP_FILES,
SHOW_SHARE_DIALOG, SHOW_SHARE_DIALOG,
CONFIRM_EMPTY_TRASH, CONFIRM_EMPTY_TRASH,
EMPTY_TRASH, EMPTY_TRASH,
@ -87,8 +88,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
case CollectionActions.CONFIRM_DELETE: case CollectionActions.CONFIRM_DELETE:
callback = confirmDeleteCollection; callback = confirmDeleteCollection;
break; break;
case CollectionActions.DELETE: case CollectionActions.DELETE_WITH_FILES:
callback = deleteCollection; callback = deleteCollectionAlongWithFiles;
break;
case CollectionActions.DELETE_BUT_KEEP_FILES:
callback = deleteCollectionButKeepFiles;
break; break;
case CollectionActions.SHOW_SHARE_DIALOG: case CollectionActions.SHOW_SHARE_DIALOG:
callback = showCollectionShareModal; callback = showCollectionShareModal;
@ -137,8 +141,13 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
} }
}; };
const deleteCollection = async () => { const deleteCollectionAlongWithFiles = async () => {
await CollectionAPI.deleteCollection(activeCollection.id); await CollectionAPI.deleteCollection(activeCollection.id, false);
redirectToAll();
};
const deleteCollectionButKeepFiles = async () => {
await CollectionAPI.deleteCollection(activeCollection.id, true);
redirectToAll(); redirectToAll();
}; };
@ -177,12 +186,21 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
const confirmDeleteCollection = () => { const confirmDeleteCollection = () => {
setDialogMessage({ setDialogMessage({
title: constants.DELETE_COLLECTION_TITLE, title: constants.DELETE_COLLECTION_TITLE,
content: constants.DELETE_COLLECTION_MESSAGE, content: constants.DELETE_COLLECTION_MESSAGE(),
proceed: { proceed: {
text: constants.DELETE_COLLECTION, text: constants.DELETE_PHOTOS,
action: handleCollectionAction(CollectionActions.DELETE), action: handleCollectionAction(
CollectionActions.DELETE_WITH_FILES
),
variant: 'danger', variant: 'danger',
}, },
secondary: {
text: constants.KEEP_PHOTOS,
action: handleCollectionAction(
CollectionActions.DELETE_BUT_KEEP_FILES
),
variant: 'primary',
},
close: { close: {
text: constants.CANCEL, text: constants.CANCEL,
}, },
@ -250,8 +268,15 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
/> />
) : collectionSummaryType === ) : collectionSummaryType ===
CollectionSummaryType.favorites ? ( CollectionSummaryType.favorites ? (
<FavoritiesCollectionOption <OnlyDownloadCollectionOption
handleCollectionAction={handleCollectionAction} handleCollectionAction={handleCollectionAction}
downloadOptionText={constants.DOWNLOAD_FAVORITES}
/>
) : collectionSummaryType ===
CollectionSummaryType.uncategorized ? (
<OnlyDownloadCollectionOption
handleCollectionAction={handleCollectionAction}
downloadOptionText={constants.DOWNLOAD_UNCATEGORIZED}
/> />
) : collectionSummaryType === ) : collectionSummaryType ===
CollectionSummaryType.incomingShare ? ( CollectionSummaryType.incomingShare ? (

View file

@ -15,7 +15,8 @@ export default function EmailShare({ collection }) {
const collectionShare: SingleInputFormProps['callback'] = async ( const collectionShare: SingleInputFormProps['callback'] = async (
email, email,
setFieldError setFieldError,
resetForm
) => { ) => {
try { try {
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
@ -28,6 +29,7 @@ export default function EmailShare({ collection }) {
} else { } else {
await shareCollection(collection, email); await shareCollection(collection, email);
await galleryContext.syncWithRemote(false, true); await galleryContext.syncWithRemote(false, true);
resetForm();
} }
} catch (e) { } catch (e) {
const errorMessage = handleSharingErrors(e); const errorMessage = handleSharingErrors(e);

View file

@ -96,6 +96,18 @@ export default function DialogBox({
{attributes.proceed.text} {attributes.proceed.text}
</Button> </Button>
)} )}
{attributes.secondary && (
<Button
size="large"
color={attributes.secondary?.variant}
onClick={() => {
attributes.secondary.action();
onClose();
}}
disabled={attributes.secondary.disabled}>
{attributes.secondary.text}
</Button>
)}
</> </>
</DialogActions> </DialogActions>
)} )}

View file

@ -3,7 +3,7 @@ import VerticallyCentered, { FlexWrapper } from 'components/Container';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import React, { useContext, useEffect } from 'react'; import React, { useContext, useEffect } from 'react';
import billingService from 'services/billingService'; import billingService from 'services/billingService';
import { getFamilyPlanAdmin } from 'utils/billing'; import { getFamilyPlanAdmin } from 'utils/user/family';
import { preloadImage } from 'utils/common'; import { preloadImage } from 'utils/common';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import DialogTitleWithCloseButton from './DialogBox/TitleWithCloseButton'; import DialogTitleWithCloseButton from './DialogBox/TitleWithCloseButton';

View file

@ -31,7 +31,7 @@ export function OverflowMenuOption({
<MenuItem <MenuItem
onClick={handleClick} onClick={handleClick}
sx={{ sx={{
width: 220, minWidth: 220,
color: (theme) => theme.palette[color].main, color: (theme) => theme.palette[color].main,
padding: 1.5, padding: 1.5,
'& .MuiSvgIcon-root': { '& .MuiSvgIcon-root': {

View file

@ -4,7 +4,6 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import DownloadManager from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import constants from 'utils/strings/constants';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import PhotoViewer from 'components/PhotoViewer'; import PhotoViewer from 'components/PhotoViewer';
import { import {
@ -13,10 +12,9 @@ import {
TRASH_SECTION, TRASH_SECTION,
} from 'constants/collection'; } from 'constants/collection';
import { isSharedFile } from 'utils/file'; import { isSharedFile } from 'utils/file';
import { isPlaybackPossible } from 'utils/photoFrame'; import { updateFileMsrcProps, updateFileSrcProps } from 'utils/photoFrame';
import { PhotoList } from './PhotoList'; import { PhotoList } from './PhotoList';
import { SelectedState } from 'types/gallery'; import { MergedSourceURL, SelectedState } from 'types/gallery';
import { FILE_TYPE } from 'constants/file';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager'; import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
@ -27,12 +25,12 @@ import { IsArchived } from 'utils/magicMetadata';
import { isSameDayAnyYear, isInsideBox } from 'utils/search'; import { isSameDayAnyYear, isInsideBox } from 'utils/search';
import { Search } from 'types/search'; import { Search } from 'types/search';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { CustomError } from 'utils/error';
import { User } from 'types/user'; import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import PhotoSwipe from 'photoswipe';
const Container = styled('div')` const Container = styled('div')`
display: block; display: block;
@ -66,19 +64,12 @@ interface Props {
deletedFileIds?: Set<number>; deletedFileIds?: Set<number>;
setDeletedFileIds?: (value: Set<number>) => void; setDeletedFileIds?: (value: Set<number>) => void;
activeCollection: number; activeCollection: number;
isSharedCollection?: boolean; isIncomingSharedCollection?: boolean;
enableDownload?: boolean; enableDownload?: boolean;
isDeduplicating?: boolean; isDeduplicating?: boolean;
resetSearch?: () => void; resetSearch?: () => void;
} }
type SourceURL = {
originalImageURL?: string;
originalVideoURL?: string;
convertedImageURL?: string;
convertedVideoURL?: string;
};
const PhotoFrame = ({ const PhotoFrame = ({
files, files,
collections, collections,
@ -95,10 +86,11 @@ const PhotoFrame = ({
deletedFileIds, deletedFileIds,
setDeletedFileIds, setDeletedFileIds,
activeCollection, activeCollection,
isSharedCollection, isIncomingSharedCollection,
enableDownload, enableDownload,
isDeduplicating, isDeduplicating,
}: Props) => { }: Props) => {
const [user, setUser] = useState<User>(null);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState<number>(0); const [currentIndex, setCurrentIndex] = useState<number>(0);
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({}); const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
@ -119,6 +111,11 @@ const PhotoFrame = ({
const [filteredData, setFilteredData] = useState<EnteFile[]>([]); const [filteredData, setFilteredData] = useState<EnteFile[]>([]);
useEffect(() => {
const user: User = getData(LS_KEYS.USER);
setUser(user);
}, []);
useEffect(() => { useEffect(() => {
const main = () => { const main = () => {
if (updateInProgress.current) { if (updateInProgress.current) {
@ -130,13 +127,6 @@ const PhotoFrame = ({
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
const filteredData = files const filteredData = files
.map((item, index) => ({
...item,
dataIndex: index,
w: window.innerWidth,
h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
}))
.filter((item) => { .filter((item) => {
if ( if (
deletedFileIds?.has(item.id) && deletedFileIds?.has(item.id) &&
@ -179,7 +169,10 @@ const PhotoFrame = ({
return false; return false;
} }
if (isSharedFile(user, item) && !isSharedCollection) { if (
isSharedFile(user, item) &&
activeCollection !== item.collectionID
) {
return false; return false;
} }
if (activeCollection === TRASH_SECTION && !item.isTrashed) { if (activeCollection === TRASH_SECTION && !item.isTrashed) {
@ -202,6 +195,31 @@ const PhotoFrame = ({
return false; return false;
} }
return false; return false;
})
.map((item) => {
const filteredItem = {
...item,
w: window.innerWidth,
h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
};
try {
if (galleryContext.thumbs.has(item.id)) {
updateFileMsrcProps(
filteredItem,
galleryContext.thumbs.get(item.id)
);
}
if (galleryContext.files.has(item.id)) {
updateFileSrcProps(
filteredItem,
galleryContext.files.get(item.id)
);
}
} catch (e) {
logError(e, 'PhotoFrame url prefill failed');
}
return filteredItem;
}); });
setFilteredData(filteredData); setFilteredData(filteredData);
updateInProgress.current = false; updateInProgress.current = false;
@ -221,6 +239,10 @@ const PhotoFrame = ({
activeCollection, activeCollection,
]); ]);
useEffect(() => {
setFetching({});
}, [filteredData]);
const fileToCollectionsMap = useMemo(() => { const fileToCollectionsMap = useMemo(() => {
const fileToCollectionsMap = new Map<number, number[]>(); const fileToCollectionsMap = new Map<number, number[]>();
files.forEach((file) => { files.forEach((file) => {
@ -273,6 +295,7 @@ const PhotoFrame = ({
}; };
document.addEventListener('keydown', handleKeyDown, false); document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('keyup', handleKeyUp, false); document.addEventListener('keyup', handleKeyUp, false);
router.events.on('hashChangeComplete', (url: string) => { router.events.on('hashChangeComplete', (url: string) => {
const start = url.indexOf('#'); const start = url.indexOf('#');
const hash = url.slice(start !== -1 ? start : url.length); const hash = url.slice(start !== -1 ? start : url.length);
@ -285,9 +308,10 @@ const PhotoFrame = ({
setOpen(false); setOpen(false);
} }
}); });
return () => { return () => {
document.addEventListener('keydown', handleKeyDown, false); document.removeEventListener('keydown', handleKeyDown, false);
document.addEventListener('keyup', handleKeyUp, false); document.removeEventListener('keyup', handleKeyUp, false);
}; };
}, []); }, []);
@ -303,127 +327,61 @@ const PhotoFrame = ({
} }
}, [search, filteredData]); }, [search, filteredData]);
const resetFetching = () => {
setFetching({});
};
useEffect(() => { useEffect(() => {
if (selected.count === 0) { if (selected.count === 0) {
setRangeStart(null); setRangeStart(null);
} }
}, [selected]); }, [selected]);
const getFileIndexFromID = (files: EnteFile[], id: number) => { const updateURL = (index: number) => (id: number, url: string) => {
const index = files.findIndex((file) => file.id === id); const file = filteredData[index];
if (index === -1) { // this is to prevent outdated updateURL call from updating the wrong file
throw CustomError.FILE_ID_NOT_FOUND; if (file.id !== id) {
addLogLine(
`PhotoSwipe: updateURL: file id mismatch: ${file.id} !== ${id}`
);
return;
} }
return index; if (file.msrc) {
addLogLine(`PhotoSwipe: updateURL: msrc already set: ${file.msrc}`);
logError(
new Error(
`PhotoSwipe: updateURL: msrc already set: ${file.msrc}`
),
'PhotoSwipe: updateURL called with msrc already set'
);
return;
}
updateFileMsrcProps(file, url);
}; };
const updateURL = (id: number) => (url: string) => { const updateSrcURL = async (
const updateFile = (file: EnteFile) => { index: number,
file.msrc = url; id: number,
file.w = window.innerWidth; mergedSrcURL: MergedSourceURL
file.h = window.innerHeight; ) => {
const file = filteredData[index];
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) { // this is to prevent outdate updateSrcURL call from updating the wrong file
file.html = ` if (file.id !== id) {
<div class="pswp-item-container"> addLogLine(
<img src="${url}" onContextMenu="return false;"/> `PhotoSwipe: updateSrcURL: file id mismatch: ${file.id} !== ${id}`
<div class="spinner-border text-light" role="status"> );
<span class="sr-only">Loading...</span> return;
</div> }
</div> if (file.isSourceLoaded) {
`; addLogLine(
} else if ( `PhotoSwipe: updateSrcURL: source already loaded: ${file.id}`
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO && );
!file.html logError(
) { new Error(
file.html = ` `PhotoSwipe: updateSrcURL: source already loaded: ${file.id}`
<div class="pswp-item-container"> ),
<img src="${url}" onContextMenu="return false;"/> 'PhotoSwipe updateSrcURL called when source already loaded'
<div class="spinner-border text-light" role="status"> );
<span class="sr-only">Loading...</span> return;
</div> }
</div> await updateFileSrcProps(file, mergedSrcURL);
`;
} else if (
file.metadata.fileType === FILE_TYPE.IMAGE &&
!file.src
) {
file.src = url;
}
return file;
};
const index = getFileIndexFromID(files, id);
return updateFile(files[index]);
};
const updateSrcURL = async (id: number, srcURL: SourceURL) => {
const {
originalImageURL,
convertedImageURL,
originalVideoURL,
convertedVideoURL,
} = srcURL;
const isPlayable =
convertedVideoURL && (await isPlaybackPossible(convertedVideoURL));
const updateFile = (file: EnteFile) => {
file.w = window.innerWidth;
file.h = window.innerHeight;
file.isSourceLoaded = true;
file.originalImageURL = originalImageURL;
file.originalVideoURL = originalVideoURL;
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
if (isPlayable) {
file.html = `
<video controls onContextMenu="return false;">
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
`;
} else {
file.html = `
<div class="pswp-item-container">
<img src="${file.msrc}" onContextMenu="return false;"/>
<div class="download-banner" >
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
<a class="btn btn-outline-success" href=${convertedVideoURL} download="${file.metadata.title}"">Download</a>
</div>
</div>
`;
}
} else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
if (isPlayable) {
file.html = `
<div class = 'pswp-item-container'>
<img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/>
<video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
</div>
`;
} else {
file.html = `
<div class="pswp-item-container">
<img src="${file.msrc}" onContextMenu="return false;"/>
<div class="download-banner">
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
<button class = "btn btn-outline-success" id = "download-btn-${file.id}">Download</button>
</div>
</div>
`;
}
} else {
file.src = convertedImageURL;
}
return file;
};
setIsSourceLoaded(true); setIsSourceLoaded(true);
const index = getFileIndexFromID(files, id);
return updateFile(files[index]);
}; };
const handleClose = (needUpdate) => { const handleClose = (needUpdate) => {
@ -436,30 +394,52 @@ const PhotoFrame = ({
setOpen(true); setOpen(true);
}; };
const handleSelect = (id: number, index?: number) => (checked: boolean) => { const handleSelect =
if (selected.collectionID !== activeCollection) { (id: number, isOwnFile: boolean, index?: number) =>
setSelected({ count: 0, collectionID: 0 }); (checked: boolean) => {
} if (typeof index !== 'undefined') {
if (typeof index !== 'undefined') { if (checked) {
if (checked) { setRangeStart(index);
setRangeStart(index); } else {
} else { setRangeStart(undefined);
setRangeStart(undefined); }
} }
} setSelected((selected) => {
if (selected.collectionID !== activeCollection) {
selected = { ownCount: 0, count: 0, collectionID: 0 };
}
setSelected((selected) => ({ const handleCounterChange = (count: number) => {
...selected, if (selected[id] === checked) {
[id]: checked, return count;
count: }
selected[id] === checked if (checked) {
? selected.count return count + 1;
: checked } else {
? selected.count + 1 return count - 1;
: selected.count - 1, }
collectionID: activeCollection, };
}));
}; const handleAllCounterChange = () => {
if (isOwnFile) {
return {
ownCount: handleCounterChange(selected.ownCount),
count: handleCounterChange(selected.count),
};
} else {
return {
count: handleCounterChange(selected.count),
};
}
};
return {
...selected,
[id]: checked,
collectionID: activeCollection,
...handleAllCounterChange(),
};
});
};
const onHoverOver = (index: number) => () => { const onHoverOver = (index: number) => () => {
setCurrentHover(index); setCurrentHover(index);
}; };
@ -481,56 +461,59 @@ const PhotoFrame = ({
(index - i) * direction > 0; (index - i) * direction > 0;
i += direction i += direction
) { ) {
handleSelect(filteredData[i].id)(!checked); handleSelect(
filteredData[i].id,
filteredData[i].ownerID === user?.id
)(!checked);
} }
handleSelect(filteredData[index].id, index)(!checked); handleSelect(
filteredData[index].id,
filteredData[index].ownerID === user?.id,
index
)(!checked);
} }
}; };
const getThumbnail = ( const getThumbnail = (
files: EnteFile[], item: EnteFile,
index: number, index: number,
isScrolling: boolean isScrolling: boolean
) => ) => (
files[index] ? ( <PreviewCard
<PreviewCard key={`tile-${item.id}-selected-${selected[item.id] ?? false}`}
key={`tile-${files[index].id}-selected-${ file={item}
selected[files[index].id] ?? false updateURL={updateURL(index)}
}`} onClick={onThumbnailClick(index)}
file={files[index]} selectable={
updateURL={updateURL(files[index].id)} !publicCollectionGalleryContext?.accessedThroughSharedURL
onClick={onThumbnailClick(index)} }
selectable={!isSharedCollection} onSelect={handleSelect(item.id, item.ownerID === user?.id, index)}
onSelect={handleSelect(files[index].id, index)} selected={
selected={ selected.collectionID === activeCollection && selected[item.id]
selected.collectionID === activeCollection && }
selected[files[index].id] selectOnClick={selected.count > 0}
} onHover={onHoverOver(index)}
selectOnClick={selected.count > 0} onRangeSelect={handleRangeSelect(index)}
onHover={onHoverOver(index)} isRangeSelectActive={isShiftKeyPressed && selected.count > 0}
onRangeSelect={handleRangeSelect(index)} isInsSelectRange={
isRangeSelectActive={isShiftKeyPressed && selected.count > 0} (index >= rangeStart && index <= currentHover) ||
isInsSelectRange={ (index >= currentHover && index <= rangeStart)
(index >= rangeStart && index <= currentHover) || }
(index >= currentHover && index <= rangeStart) activeCollection={activeCollection}
} showPlaceholder={isScrolling}
activeCollection={activeCollection} />
showPlaceholder={isScrolling} );
/>
) : (
<></>
);
const getSlideData = async ( const getSlideData = async (
instance: any, instance: PhotoSwipe<PhotoSwipe.Options>,
index: number, index: number,
item: EnteFile item: EnteFile
) => { ) => {
addLogLine( addLogLine(
`[${ `[${
item.id item.id
}] getSlideData called for thumbnail:${!!item.msrc} original:${ }] getSlideData called for thumbnail:${!!item.msrc} sourceLoaded:${isSourceLoaded} fetching:${
!!item.msrc && item.src !== item.msrc fetching[item.id]
} inProgress:${fetching[item.id]}` }`
); );
if (!item.msrc) { if (!item.msrc) {
addLogLine(`[${item.id}] doesn't have thumbnail`); addLogLine(`[${item.id}] doesn't have thumbnail`);
@ -559,22 +542,15 @@ const PhotoFrame = ({
} }
galleryContext.thumbs.set(item.id, url); galleryContext.thumbs.set(item.id, url);
} }
const newFile = updateURL(item.id)(url); updateURL(index)(item.id, url);
item.msrc = newFile.msrc;
item.html = newFile.html;
item.src = newFile.src;
item.isSourceLoaded = newFile.isSourceLoaded;
item.originalImageURL = newFile.originalImageURL;
item.originalVideoURL = newFile.originalVideoURL;
item.w = newFile.w;
item.h = newFile.h;
addLogLine(
`[${item.id}] calling invalidateCurrItems for thumbnail`
);
try { try {
addLogLine(
`[${
item.id
}] calling invalidateCurrItems for thumbnail msrc :${!!item.msrc}`
);
instance.invalidateCurrItems(); instance.invalidateCurrItems();
if (instance.isOpen()) { if ((instance as any).isOpen()) {
instance.updateSize(true); instance.updateSize(true);
} }
} catch (e) { } catch (e) {
@ -588,100 +564,76 @@ const PhotoFrame = ({
logError(e, 'getSlideData failed get msrc url failed'); logError(e, 'getSlideData failed get msrc url failed');
} }
} }
if (!fetching[item.id]) { if (item.isSourceLoaded) {
addLogLine(`[${item.id}] new file download fetch original request`); addLogLine(`[${item.id}] source already loaded`);
try { return;
fetching[item.id] = true; }
let urls: { original: string[]; converted: string[] }; if (fetching[item.id]) {
if (galleryContext.files.has(item.id)) { addLogLine(`[${item.id}] file download already in progress`);
addLogLine( return;
`[${item.id}] gallery context cache hit, using cached file` }
); try {
const mergedURL = galleryContext.files.get(item.id); addLogLine(`[${item.id}] new file src request`);
urls = { fetching[item.id] = true;
original: mergedURL.original.split(','), let srcURL: MergedSourceURL;
converted: mergedURL.converted.split(','), if (galleryContext.files.has(item.id)) {
}; addLogLine(
} else { `[${item.id}] gallery context cache hit, using cached file`
addLogLine( );
`[${item.id}] gallery context cache miss, calling downloadManager to get file` srcURL = galleryContext.files.get(item.id);
); } else {
appContext.startLoading(); addLogLine(
if ( `[${item.id}] gallery context cache miss, calling downloadManager to get file`
publicCollectionGalleryContext.accessedThroughSharedURL );
) { appContext.startLoading();
urls = await PublicCollectionDownloadManager.getFile( let downloadedURL;
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
downloadedURL =
await PublicCollectionDownloadManager.getFile(
item, item,
publicCollectionGalleryContext.token, publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken, publicCollectionGalleryContext.passwordToken,
true true
); );
} else {
urls = await DownloadManager.getFile(item, true);
}
appContext.finishLoading();
const mergedURL = {
original: urls.original.join(','),
converted: urls.converted.join(','),
};
galleryContext.files.set(item.id, mergedURL);
}
let originalImageURL;
let originalVideoURL;
let convertedImageURL;
let convertedVideoURL;
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
[originalImageURL, originalVideoURL] = urls.original;
[convertedImageURL, convertedVideoURL] = urls.converted;
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
[originalVideoURL] = urls.original;
[convertedVideoURL] = urls.converted;
} else { } else {
[originalImageURL] = urls.original; downloadedURL = await DownloadManager.getFile(item, true);
[convertedImageURL] = urls.converted;
} }
setIsSourceLoaded(false); appContext.finishLoading();
const newFile = await updateSrcURL(item.id, { const mergedURL: MergedSourceURL = {
originalImageURL, original: downloadedURL.original.join(','),
originalVideoURL, converted: downloadedURL.converted.join(','),
convertedImageURL, };
convertedVideoURL, galleryContext.files.set(item.id, mergedURL);
}); srcURL = mergedURL;
item.msrc = newFile.msrc; }
item.html = newFile.html; setIsSourceLoaded(false);
item.src = newFile.src; await updateSrcURL(index, item.id, srcURL);
item.isSourceLoaded = newFile.isSourceLoaded;
item.originalImageURL = newFile.originalImageURL; try {
item.originalVideoURL = newFile.originalVideoURL; addLogLine(
item.w = newFile.w; `[${item.id}] calling invalidateCurrItems for src, source loaded :${item.isSourceLoaded}`
item.h = newFile.h; );
try { instance.invalidateCurrItems();
addLogLine( if ((instance as any).isOpen()) {
`[${item.id}] calling invalidateCurrItems for src` instance.updateSize(true);
);
instance.invalidateCurrItems();
if (instance.isOpen()) {
instance.updateSize(true);
}
} catch (e) {
logError(
e,
'updating photoswipe after src url update failed'
);
throw e;
} }
} catch (e) { } catch (e) {
logError(e, 'getSlideData failed get src url failed'); logError(e, 'updating photoswipe after src url update failed');
fetching[item.id] = false; throw e;
// no-op
} }
} catch (e) {
logError(e, 'getSlideData failed get src url failed');
fetching[item.id] = false;
// no-op
} }
}; };
return ( return (
<> <>
{!isFirstLoad && files.length === 0 && !isInSearchMode ? ( {!isFirstLoad &&
files.length === 0 &&
!isInSearchMode &&
activeCollection === ALL_SECTION ? (
<EmptyScreen openUploader={openUploader} /> <EmptyScreen openUploader={openUploader} />
) : ( ) : (
<Container> <Container>
@ -698,7 +650,6 @@ const PhotoFrame = ({
!isInSearchMode && !isInSearchMode &&
!deduplicateContext.isOnDeduplicatePage !deduplicateContext.isOnDeduplicatePage
} }
resetFetching={resetFetching}
/> />
)} )}
</AutoSizer> </AutoSizer>
@ -711,7 +662,7 @@ const PhotoFrame = ({
favItemIds={favItemIds} favItemIds={favItemIds}
deletedFileIds={deletedFileIds} deletedFileIds={deletedFileIds}
setDeletedFileIds={setDeletedFileIds} setDeletedFileIds={setDeletedFileIds}
isSharedCollection={isSharedCollection} isIncomingSharedCollection={isIncomingSharedCollection}
isTrashCollection={activeCollection === TRASH_SECTION} isTrashCollection={activeCollection === TRASH_SECTION}
enableDownload={enableDownload} enableDownload={enableDownload}
isSourceLoaded={isSourceLoaded} isSourceLoaded={isSourceLoaded}

View file

@ -163,12 +163,11 @@ interface Props {
filteredData: EnteFile[]; filteredData: EnteFile[];
showAppDownloadBanner: boolean; showAppDownloadBanner: boolean;
getThumbnail: ( getThumbnail: (
files: EnteFile[], file: EnteFile,
index: number, index: number,
isScrolling?: boolean isScrolling?: boolean
) => JSX.Element; ) => JSX.Element;
activeCollection: number; activeCollection: number;
resetFetching: () => void;
} }
export function PhotoList({ export function PhotoList({
@ -178,7 +177,6 @@ export function PhotoList({
showAppDownloadBanner, showAppDownloadBanner,
getThumbnail, getThumbnail,
activeCollection, activeCollection,
resetFetching,
}: Props) { }: Props) {
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);
const publicCollectionGalleryContext = useContext( const publicCollectionGalleryContext = useContext(
@ -205,7 +203,6 @@ export function PhotoList({
const refreshList = () => { const refreshList = () => {
listRef.current?.resetAfterIndex(0); listRef.current?.resetAfterIndex(0);
resetFetching();
}; };
useEffect(() => { useEffect(() => {
@ -657,7 +654,7 @@ export function PhotoList({
case ITEM_TYPE.FILE: { case ITEM_TYPE.FILE: {
const ret = listItem.items.map((item, idx) => const ret = listItem.items.map((item, idx) =>
getThumbnail( getThumbnail(
filteredData, item,
listItem.itemStartIndex + idx, listItem.itemStartIndex + idx,
isScrolling isScrolling
) )
@ -666,7 +663,11 @@ export function PhotoList({
let sum = 0; let sum = 0;
for (let i = 0; i < listItem.groups.length - 1; i++) { for (let i = 0; i < listItem.groups.length - 1; i++) {
sum = sum + listItem.groups[i]; sum = sum + listItem.groups[i];
ret.splice(sum, 0, <div />); ret.splice(
sum,
0,
<div key={`${listItem.items[0].id}-gap-${i}`} />
);
sum += 1; sum += 1;
} }
} }
@ -690,7 +691,7 @@ export function PhotoList({
width={width} width={width}
itemCount={timeStampList.length} itemCount={timeStampList.length}
itemKey={generateKey} itemKey={generateKey}
overscanCount={0} overscanCount={3}
useIsScrolling> useIsScrolling>
{({ index, style, isScrolling }) => ( {({ index, style, isScrolling }) => (
<ListItem style={style}> <ListItem style={style}>

View file

@ -68,7 +68,7 @@ interface Iprops {
favItemIds: Set<number>; favItemIds: Set<number>;
deletedFileIds: Set<number>; deletedFileIds: Set<number>;
setDeletedFileIds?: (value: Set<number>) => void; setDeletedFileIds?: (value: Set<number>) => void;
isSharedCollection: boolean; isIncomingSharedCollection: boolean;
isTrashCollection: boolean; isTrashCollection: boolean;
enableDownload: boolean; enableDownload: boolean;
isSourceLoaded: boolean; isSourceLoaded: boolean;
@ -163,10 +163,6 @@ function PhotoViewer(props: Iprops) {
}; };
}, [isOpen, photoSwipe, showInfo]); }, [isOpen, photoSwipe, showInfo]);
useEffect(() => {
updateItems(items);
}, [items]);
useEffect(() => { useEffect(() => {
if (photoSwipe) { if (photoSwipe) {
photoSwipe.options.arrowKeys = !showInfo; photoSwipe.options.arrowKeys = !showInfo;
@ -386,42 +382,64 @@ function PhotoViewer(props: Iprops) {
const trashFile = async (file: EnteFile) => { const trashFile = async (file: EnteFile) => {
const { deletedFileIds, setDeletedFileIds } = props; const { deletedFileIds, setDeletedFileIds } = props;
deletedFileIds.add(file.id); try {
setDeletedFileIds(new Set(deletedFileIds)); appContext.startLoading();
await trashFiles([file]); await trashFiles([file]);
needUpdate.current = true; appContext.finishLoading();
deletedFileIds.add(file.id);
setDeletedFileIds(new Set(deletedFileIds));
updateItems(props.items.filter((item) => item.id !== file.id));
needUpdate.current = true;
} catch (e) {
logError(e, 'trashFile failed');
}
}; };
const confirmTrashFile = (file: EnteFile) => { const confirmTrashFile = (file: EnteFile) => {
if (!file || props.isSharedCollection || props.isTrashCollection) { if (
!file ||
props.isIncomingSharedCollection ||
props.isTrashCollection
) {
return; return;
} }
appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file))); appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
}; };
const updateItems = (items = []) => { const updateItems = (items: EnteFile[]) => {
if (photoSwipe) { try {
if (items.length === 0) { if (photoSwipe) {
photoSwipe.close(); if (items.length === 0) {
} photoSwipe.close();
photoSwipe.items.length = 0; }
items.forEach((item) => { photoSwipe.items.length = 0;
photoSwipe.items.push(item); items.forEach((item) => {
}); photoSwipe.items.push(item);
photoSwipe.invalidateCurrItems(); });
if (isOpen) {
photoSwipe.updateSize(true); photoSwipe.invalidateCurrItems();
if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) { if (isOpen) {
photoSwipe.goTo(0); photoSwipe.updateSize(true);
if (
photoSwipe.getCurrentIndex() >= photoSwipe.items.length
) {
photoSwipe.goTo(0);
}
} }
} }
} catch (e) {
logError(e, 'updateItems failed');
} }
}; };
const refreshPhotoswipe = () => { const refreshPhotoswipe = () => {
photoSwipe.invalidateCurrItems(); try {
if (isOpen) { photoSwipe.invalidateCurrItems();
photoSwipe.updateSize(true); if (isOpen) {
photoSwipe.updateSize(true);
}
} catch (e) {
logError(e, 'refreshPhotoswipe failed');
} }
}; };
@ -560,7 +578,7 @@ function PhotoViewer(props: Iprops) {
<ContentCopy fontSize="small" /> <ContentCopy fontSize="small" />
</button> </button>
)} )}
{!props.isSharedCollection && {!props.isIncomingSharedCollection &&
!props.isTrashCollection && ( !props.isTrashCollection && (
<button <button
className="pswp__button pswp__button--custom" className="pswp__button pswp__button--custom"
@ -582,7 +600,7 @@ function PhotoViewer(props: Iprops) {
title={constants.TOGGLE_FULLSCREEN} title={constants.TOGGLE_FULLSCREEN}
/> />
{!props.isSharedCollection && ( {!props.isIncomingSharedCollection && (
<button <button
className="pswp__button pswp__button--custom" className="pswp__button pswp__button--custom"
title={constants.INFO_OPTION} title={constants.INFO_OPTION}
@ -590,7 +608,7 @@ function PhotoViewer(props: Iprops) {
<InfoIcon fontSize="small" /> <InfoIcon fontSize="small" />
</button> </button>
)} )}
{!props.isSharedCollection && {!props.isIncomingSharedCollection &&
!props.isTrashCollection && ( !props.isTrashCollection && (
<button <button
title={ title={
@ -641,7 +659,7 @@ function PhotoViewer(props: Iprops) {
</div> </div>
<FileInfo <FileInfo
isTrashCollection={props.isTrashCollection} isTrashCollection={props.isTrashCollection}
shouldDisableEdits={props.isSharedCollection} shouldDisableEdits={props.isIncomingSharedCollection}
showInfo={showInfo} showInfo={showInfo}
handleCloseInfo={handleCloseInfo} handleCloseInfo={handleCloseInfo}
file={photoSwipe?.currItem as EnteFile} file={photoSwipe?.currItem as EnteFile}

View file

@ -20,12 +20,11 @@ const ShortcutButton: FC<ButtonProps<'button', Iprops>> = ({
sx={{ fontWeight: 'normal' }} sx={{ fontWeight: 'normal' }}
{...props}> {...props}>
{label} {label}
{count > 0 && (
<Box sx={{ color: 'text.secondary' }}> <Box sx={{ color: 'text.secondary' }}>
<DotSeparator /> <DotSeparator />
{count} {count}
</Box> </Box>
)}
</SidebarButton> </SidebarButton>
); );
}; };

View file

@ -1,11 +1,17 @@
import React, { useContext } from 'react'; import React, { useContext, useState, useEffect } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection'; import {
ARCHIVE_SECTION,
DUMMY_UNCATEGORIZED_SECTION,
TRASH_SECTION,
} from 'constants/collection';
import { CollectionSummaries } from 'types/collection'; import { CollectionSummaries } from 'types/collection';
import ShortcutButton from './ShortcutButton'; import ShortcutButton from './ShortcutButton';
import DeleteOutline from '@mui/icons-material/DeleteOutline'; import DeleteOutline from '@mui/icons-material/DeleteOutline';
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined'; import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
import CategoryIcon from '@mui/icons-material/Category';
import { getUncategorizedCollection } from 'services/collectionService';
interface Iprops { interface Iprops {
closeSidebar: () => void; closeSidebar: () => void;
collectionSummaries: CollectionSummaries; collectionSummaries: CollectionSummaries;
@ -16,6 +22,24 @@ export default function ShortcutSection({
collectionSummaries, collectionSummaries,
}: Iprops) { }: Iprops) {
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);
const [uncategorizedCollectionId, setUncategorizedCollectionID] =
useState<number>();
useEffect(() => {
const main = async () => {
const unCategorizedCollection = await getUncategorizedCollection();
if (unCategorizedCollection) {
setUncategorizedCollectionID(unCategorizedCollection.id);
} else {
setUncategorizedCollectionID(DUMMY_UNCATEGORIZED_SECTION);
}
};
main();
}, []);
const openUncategorizedSection = () => {
galleryContext.setActiveCollection(uncategorizedCollectionId);
closeSidebar();
};
const openTrashSection = () => { const openTrashSection = () => {
galleryContext.setActiveCollection(TRASH_SECTION); galleryContext.setActiveCollection(TRASH_SECTION);
@ -26,9 +50,17 @@ export default function ShortcutSection({
galleryContext.setActiveCollection(ARCHIVE_SECTION); galleryContext.setActiveCollection(ARCHIVE_SECTION);
closeSidebar(); closeSidebar();
}; };
return ( return (
<> <>
<ShortcutButton
startIcon={<CategoryIcon />}
label={constants.UNCATEGORIZED}
onClick={openUncategorizedSection}
count={
collectionSummaries.get(uncategorizedCollectionId)
?.fileCount
}
/>
<ShortcutButton <ShortcutButton
startIcon={<DeleteOutline />} startIcon={<DeleteOutline />}
label={constants.TRASH} label={constants.TRASH}

View file

@ -1,6 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { UserDetails } from 'types/user'; import { UserDetails } from 'types/user';
import { isPartOfFamily } from 'utils/billing'; import { isPartOfFamily } from 'utils/user/family';
import StorageSection from '../storageSection'; import StorageSection from '../storageSection';
import { FamilyUsageSection } from './usageSection'; import { FamilyUsageSection } from './usageSection';

View file

@ -1,7 +1,7 @@
import { IndividualSubscriptionCardContent } from './individual'; import { IndividualSubscriptionCardContent } from './individual';
import { FamilySubscriptionCardContent } from './family'; import { FamilySubscriptionCardContent } from './family';
import React from 'react'; import React from 'react';
import { hasNonAdminFamilyMembers } from 'utils/billing'; import { hasNonAdminFamilyMembers } from 'utils/user/family';
import { Overlay, SpaceBetweenFlex } from 'components/Container'; import { Overlay, SpaceBetweenFlex } from 'components/Container';
import { UserDetails } from 'types/user'; import { UserDetails } from 'types/user';

View file

@ -2,9 +2,7 @@ import { GalleryContext } from 'pages/gallery';
import React, { MouseEventHandler, useContext, useMemo } from 'react'; import React, { MouseEventHandler, useContext, useMemo } from 'react';
import { import {
hasPaidSubscription, hasPaidSubscription,
isFamilyAdmin,
isOnFreePlan, isOnFreePlan,
isPartOfFamily,
hasExceededStorageQuota, hasExceededStorageQuota,
isSubscriptionActive, isSubscriptionActive,
isSubscriptionCancelled, isSubscriptionCancelled,
@ -15,6 +13,7 @@ import { UserDetails } from 'types/user';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { Typography } from '@mui/material'; import { Typography } from '@mui/material';
import billingService from 'services/billingService'; import billingService from 'services/billingService';
import { isPartOfFamily, isFamilyAdmin } from 'utils/user/family';
export default function SubscriptionStatus({ export default function SubscriptionStatus({
userDetails, userDetails,

View file

@ -9,7 +9,7 @@ import SubscriptionStatus from './SubscriptionStatus';
import { Box, Skeleton } from '@mui/material'; import { Box, Skeleton } from '@mui/material';
import { MemberSubscriptionManage } from '../MemberSubscriptionManage'; import { MemberSubscriptionManage } from '../MemberSubscriptionManage';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { isPartOfFamily, isFamilyAdmin } from 'utils/billing'; import { isFamilyAdmin, isPartOfFamily } from 'utils/user/family';
export default function UserDetailsSection({ sidebarView }) { export default function UserDetailsSection({ sidebarView }) {
const galleryContext = useContext(GalleryContext); const galleryContext = useContext(GalleryContext);

View file

@ -1,6 +1,6 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { Formik, FormikHelpers } from 'formik'; import { Formik, FormikHelpers, FormikState } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import SubmitButton from './SubmitButton'; import SubmitButton from './SubmitButton';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
@ -14,7 +14,8 @@ interface formValues {
export interface SingleInputFormProps { export interface SingleInputFormProps {
callback: ( callback: (
inputValue: string, inputValue: string,
setFieldError: (errorMessage: string) => void setFieldError: (errorMessage: string) => void,
resetForm: (nextState?: Partial<FormikState<formValues>>) => void
) => Promise<void>; ) => Promise<void>;
fieldType: 'text' | 'email' | 'password'; fieldType: 'text' | 'email' | 'password';
placeholder: string; placeholder: string;
@ -43,10 +44,11 @@ export default function SingleInputForm(props: SingleInputFormProps) {
{ setFieldError, resetForm }: FormikHelpers<formValues> { setFieldError, resetForm }: FormikHelpers<formValues>
) => { ) => {
SetLoading(true); SetLoading(true);
await props.callback(values.inputValue, (message) => await props.callback(
setFieldError('inputValue', message) values.inputValue,
(message) => setFieldError('inputValue', message),
resetForm
); );
resetForm();
SetLoading(false); SetLoading(false);
}; };

View file

@ -533,7 +533,7 @@ export default function Uploader(props: Props) {
} }
}; };
function showUserFacingError(err: CustomError) { function showUserFacingError(err: string) {
let notification: NotificationAttributes; let notification: NotificationAttributes;
switch (err) { switch (err) {
case CustomError.SESSION_EXPIRED: case CustomError.SESSION_EXPIRED:

View file

@ -11,8 +11,6 @@ import {
hasMobileSubscription, hasMobileSubscription,
getLocalUserSubscription, getLocalUserSubscription,
hasPaidSubscription, hasPaidSubscription,
getTotalFamilyUsage,
isPartOfFamily,
isSubscriptionActive, isSubscriptionActive,
} from 'utils/billing'; } from 'utils/billing';
import { reverseString } from 'utils/common'; import { reverseString } from 'utils/common';
@ -28,6 +26,7 @@ import { getLocalUserDetails } from 'utils/user';
import { PLAN_PERIOD } from 'constants/gallery'; import { PLAN_PERIOD } from 'constants/gallery';
import FreeSubscriptionPlanSelectorCard from './free'; import FreeSubscriptionPlanSelectorCard from './free';
import PaidSubscriptionPlanSelectorCard from './paid'; import PaidSubscriptionPlanSelectorCard from './paid';
import { isPartOfFamily, getTotalFamilyUsage } from 'utils/user/family';
interface Props { interface Props {
closeModal: any; closeModal: any;

View file

@ -1,4 +1,4 @@
import React, { useContext, useLayoutEffect, useState } from 'react'; import React, { useContext, useEffect, useRef, useState } from 'react';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined'; import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined';
@ -18,7 +18,7 @@ import { formatDateRelative } from 'utils/time/format';
interface IProps { interface IProps {
file: EnteFile; file: EnteFile;
updateURL: (url: string) => EnteFile; updateURL: (id: number, url: string) => void;
onClick: () => void; onClick: () => void;
selectable: boolean; selectable: boolean;
selected: boolean; selected: boolean;
@ -197,8 +197,8 @@ const Cont = styled('div')<{ disabled: boolean }>`
`; `;
export default function PreviewCard(props: IProps) { export default function PreviewCard(props: IProps) {
const [imgSrc, setImgSrc] = useState<string>();
const { thumbs } = useContext(GalleryContext); const { thumbs } = useContext(GalleryContext);
const { const {
file, file,
onClick, onClick,
@ -212,56 +212,57 @@ export default function PreviewCard(props: IProps) {
isRangeSelectActive, isRangeSelectActive,
isInsSelectRange, isInsSelectRange,
} = props; } = props;
const [imgSrc, setImgSrc] = useState<string>(file.msrc);
const publicCollectionGalleryContext = useContext( const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext PublicCollectionGalleryContext
); );
const deduplicateContext = useContext(DeduplicateContext); const deduplicateContext = useContext(DeduplicateContext);
useLayoutEffect(() => { const isMounted = useRef(true);
if (file && !file.msrc && !props.showPlaceholder) {
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
if (!file.msrc && !props.showPlaceholder) {
const main = async () => { const main = async () => {
try { try {
let url; let url;
if ( if (thumbs.has(file.id)) {
publicCollectionGalleryContext.accessedThroughSharedURL url = thumbs.get(file.id);
) {
url =
await PublicCollectionDownloadManager.getThumbnail(
file,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
} else { } else {
url = await DownloadManager.getThumbnail(file); if (
publicCollectionGalleryContext.accessedThroughSharedURL
) {
url =
await PublicCollectionDownloadManager.getThumbnail(
file,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
} else {
url = await DownloadManager.getThumbnail(file);
}
thumbs.set(file.id, url);
}
if (!isMounted.current) {
return;
} }
setImgSrc(url); setImgSrc(url);
thumbs.set(file.id, url); updateURL(file.id, url);
const newFile = updateURL(url);
file.msrc = newFile.msrc;
file.html = newFile.html;
file.src = newFile.src;
file.w = newFile.w;
file.h = newFile.h;
} catch (e) { } catch (e) {
logError(e, 'preview card useEffect failed'); logError(e, 'preview card useEffect failed');
// no-op // no-op
} }
}; };
main();
if (thumbs.has(file.id)) {
const thumbImgSrc = thumbs.get(file.id);
setImgSrc(thumbImgSrc);
const newFile = updateURL(thumbImgSrc);
file.msrc = newFile.msrc;
file.html = newFile.html;
file.src = newFile.src;
file.w = newFile.w;
file.h = newFile.h;
} else {
main();
}
} }
}, [file, props.showPlaceholder]); }, [props.showPlaceholder]);
const handleClick = () => { const handleClick = () => {
if (selectOnClick) { if (selectOnClick) {
@ -294,7 +295,7 @@ export default function PreviewCard(props: IProps) {
return ( return (
<Cont <Cont
id={`thumb-${file?.id}-${props.showPlaceholder}`} key={`thumb-${file.id}-${props.showPlaceholder}`}
onClick={handleClick} onClick={handleClick}
onMouseEnter={handleHover} onMouseEnter={handleHover}
disabled={!file?.msrc && !imgSrc} disabled={!file?.msrc && !imgSrc}

View file

@ -35,11 +35,14 @@ interface Props {
fixTimeHelper: () => void; fixTimeHelper: () => void;
downloadHelper: () => void; downloadHelper: () => void;
count: number; count: number;
ownCount: number;
clearSelection: () => void; clearSelection: () => void;
archiveFilesHelper: () => void; archiveFilesHelper: () => void;
unArchiveFilesHelper: () => void; unArchiveFilesHelper: () => void;
activeCollection: number; activeCollection: number;
isFavoriteCollection: boolean; isFavoriteCollection: boolean;
isUncategorizedCollection: boolean;
isIncomingSharedCollection: boolean;
} }
const SelectedFileOptions = ({ const SelectedFileOptions = ({
@ -53,11 +56,14 @@ const SelectedFileOptions = ({
deleteFileHelper, deleteFileHelper,
downloadHelper, downloadHelper,
count, count,
ownCount,
clearSelection, clearSelection,
archiveFilesHelper, archiveFilesHelper,
unArchiveFilesHelper, unArchiveFilesHelper,
activeCollection, activeCollection,
isFavoriteCollection, isFavoriteCollection,
isUncategorizedCollection,
isIncomingSharedCollection,
}: Props) => { }: Props) => {
const { setDialogMessage } = useContext(AppContext); const { setDialogMessage } = useContext(AppContext);
const addToCollection = () => const addToCollection = () =>
@ -92,18 +98,33 @@ const SelectedFileOptions = ({
title: constants.RESTORE_TO_COLLECTION, title: constants.RESTORE_TO_COLLECTION,
}); });
const removeFromCollectionHandler = () => const removeFromCollectionHandler = () => {
setDialogMessage({ if (ownCount === count) {
title: constants.CONFIRM_REMOVE, setDialogMessage({
content: constants.CONFIRM_REMOVE_MESSAGE(), title: constants.REMOVE_FROM_COLLECTION,
content: constants.CONFIRM_SELF_REMOVE_MESSAGE(),
proceed: { proceed: {
action: removeFromCollectionHelper, action: removeFromCollectionHelper,
text: constants.REMOVE, text: constants.YES_REMOVE,
variant: 'danger', variant: 'primary',
}, },
close: { text: constants.CANCEL }, close: { text: constants.CANCEL },
}); });
} else {
setDialogMessage({
title: constants.REMOVE_FROM_COLLECTION,
content: constants.CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE(),
proceed: {
action: removeFromCollectionHelper,
text: constants.YES_REMOVE,
variant: 'danger',
},
close: { text: constants.CANCEL },
});
}
};
const moveToCollection = () => { const moveToCollection = () => {
setCollectionSelectorAttributes({ setCollectionSelectorAttributes({
@ -121,7 +142,8 @@ const SelectedFileOptions = ({
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<Box ml={1.5}> <Box ml={1.5}>
{count} {constants.SELECTED} {count} {constants.SELECTED}{' '}
{ownCount !== count && `(${ownCount} ${constants.YOURS})`}
</Box> </Box>
</FluidContainer> </FluidContainer>
<Stack spacing={2} direction="row" mr={2}> <Stack spacing={2} direction="row" mr={2}>
@ -138,6 +160,30 @@ const SelectedFileOptions = ({
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</> </>
) : isUncategorizedCollection ? (
<>
<Tooltip title={constants.DOWNLOAD}>
<IconButton onClick={downloadHelper}>
<DownloadIcon />
</IconButton>
</Tooltip>
<Tooltip title={constants.MOVE}>
<IconButton onClick={moveToCollection}>
<MoveIcon />
</IconButton>
</Tooltip>
<Tooltip title={constants.DELETE}>
<IconButton onClick={trashHandler}>
<DeleteIcon />
</IconButton>
</Tooltip>
</>
) : isIncomingSharedCollection ? (
<Tooltip title={constants.DOWNLOAD}>
<IconButton onClick={downloadHelper}>
<DownloadIcon />
</IconButton>
</Tooltip>
) : ( ) : (
<> <>
<Tooltip title={constants.FIX_CREATION_TIME}> <Tooltip title={constants.FIX_CREATION_TIME}>

View file

@ -1,11 +1,12 @@
export const ARCHIVE_SECTION = -1; export const ARCHIVE_SECTION = -1;
export const TRASH_SECTION = -2; export const TRASH_SECTION = -2;
export const DUMMY_UNCATEGORIZED_SECTION = -3;
export const ALL_SECTION = 0; export const ALL_SECTION = 0;
export enum CollectionType { export enum CollectionType {
folder = 'folder', folder = 'folder',
favorites = 'favorites', favorites = 'favorites',
album = 'album', album = 'album',
uncategorized = 'uncategorized',
} }
export enum CollectionSummaryType { export enum CollectionSummaryType {
@ -14,6 +15,7 @@ export enum CollectionSummaryType {
album = 'album', album = 'album',
archive = 'archive', archive = 'archive',
trash = 'trash', trash = 'trash',
uncategorized = 'uncategorized',
all = 'all', all = 'all',
outgoingShare = 'outgoingShare', outgoingShare = 'outgoingShare',
incomingShare = 'incomingShare', incomingShare = 'incomingShare',
@ -26,6 +28,9 @@ export enum COLLECTION_SORT_BY {
UPDATION_TIME_DESCENDING, UPDATION_TIME_DESCENDING,
} }
export const UNCATEGORIZED_COLLECTION_NAME = 'Uncategorized';
export const FAVORITE_COLLECTION_NAME = 'Favorites';
export const COLLECTION_SHARE_DEFAULT_VALID_DURATION = export const COLLECTION_SHARE_DEFAULT_VALID_DURATION =
10 * 24 * 60 * 60 * 1000 * 1000; 10 * 24 * 60 * 60 * 1000 * 1000;
export const COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT = 4; export const COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT = 4;
@ -41,21 +46,22 @@ export const COLLECTION_SORT_ORDER = new Map([
[CollectionSummaryType.archived, 2], [CollectionSummaryType.archived, 2],
[CollectionSummaryType.archive, 3], [CollectionSummaryType.archive, 3],
[CollectionSummaryType.trash, 4], [CollectionSummaryType.trash, 4],
[CollectionSummaryType.uncategorized, 4],
]); ]);
export const SYSTEM_COLLECTION_TYPES = new Set([ export const SYSTEM_COLLECTION_TYPES = new Set([
CollectionSummaryType.all, CollectionSummaryType.all,
CollectionSummaryType.archive, CollectionSummaryType.archive,
CollectionSummaryType.trash, CollectionSummaryType.trash,
CollectionSummaryType.uncategorized,
]); ]);
export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([ export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([
CollectionSummaryType.all, CollectionSummaryType.all,
CollectionSummaryType.archive, CollectionSummaryType.archive,
CollectionSummaryType.incomingShare, CollectionSummaryType.incomingShare,
CollectionSummaryType.outgoingShare,
CollectionSummaryType.sharedOnlyViaLink,
CollectionSummaryType.trash, CollectionSummaryType.trash,
CollectionSummaryType.uncategorized,
]); ]);
export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([ export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([
@ -66,4 +72,5 @@ export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([
export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([ export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([
CollectionSummaryType.trash, CollectionSummaryType.trash,
CollectionSummaryType.archive, CollectionSummaryType.archive,
CollectionSummaryType.uncategorized,
]); ]);

View file

@ -15,6 +15,8 @@ export const FILE_TYPE_LIB_MISSED_FORMATS = [
{ fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' }, { fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' },
]; ];
export const KNOWN_NON_MEDIA_FORMATS = ['xmp'];
export const EXIFLESS_FORMATS = ['image/gif']; export const EXIFLESS_FORMATS = ['image/gif'];
export const EXIF_LIBRARY_UNSUPPORTED_FORMATS = ['image/webp']; export const EXIF_LIBRARY_UNSUPPORTED_FORMATS = ['image/webp'];

View file

@ -257,8 +257,10 @@ export default function App({ Component, err }) {
isLoadingBarRunning.current = true; isLoadingBarRunning.current = true;
}; };
const finishLoading = () => { const finishLoading = () => {
isLoadingBarRunning.current && loadingBar.current?.complete(); setTimeout(() => {
isLoadingBarRunning.current = false; isLoadingBarRunning.current && loadingBar.current?.complete();
isLoadingBarRunning.current = false;
}, 100);
}; };
const closeMessageDialog = () => setMessageDialogView(false); const closeMessageDialog = () => setMessageDialogView(false);

View file

@ -54,6 +54,7 @@ export default function Deduplicate() {
const [selected, setSelected] = useState<SelectedState>({ const [selected, setSelected] = useState<SelectedState>({
count: 0, count: 0,
collectionID: 0, collectionID: 0,
ownCount: 0,
}); });
const closeDeduplication = function () { const closeDeduplication = function () {
Router.push(PAGES.GALLERY); Router.push(PAGES.GALLERY);
@ -107,6 +108,7 @@ export default function Deduplicate() {
setFileSizeMap(currFileSizeMap); setFileSizeMap(currFileSizeMap);
const selectedFiles = { const selectedFiles = {
count: count, count: count,
ownCount: count,
collectionID: ALL_SECTION, collectionID: ALL_SECTION,
}; };
for (const fileID of toSelectFileIDs) { for (const fileID of toSelectFileIDs) {
@ -144,7 +146,7 @@ export default function Deduplicate() {
}; };
const clearSelection = function () { const clearSelection = function () {
setSelected({ count: 0, collectionID: 0 }); setSelected({ count: 0, collectionID: 0, ownCount: 0 });
}; };
if (!duplicateFiles) { if (!duplicateFiles) {

View file

@ -60,18 +60,19 @@ import Uploader from 'components/Upload/Uploader';
import { import {
ALL_SECTION, ALL_SECTION,
ARCHIVE_SECTION, ARCHIVE_SECTION,
CollectionSummaryType,
CollectionType, CollectionType,
DUMMY_UNCATEGORIZED_SECTION,
TRASH_SECTION, TRASH_SECTION,
UNCATEGORIZED_COLLECTION_NAME,
} from 'constants/collection'; } from 'constants/collection';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { CustomError, ServerErrorCodes } from 'utils/error'; import { CustomError, ServerErrorCodes } from 'utils/error';
import { PAGES } from 'constants/pages'; import { PAGES } from 'constants/pages';
import { import {
COLLECTION_OPS_TYPE, COLLECTION_OPS_TYPE,
isSharedCollection,
handleCollectionOps, handleCollectionOps,
getSelectedCollection, getSelectedCollection,
isFavoriteCollection,
getArchivedCollections, getArchivedCollections,
hasNonSystemCollections, hasNonSystemCollections,
} from 'utils/collection'; } from 'utils/collection';
@ -87,7 +88,7 @@ import FixCreationTime, {
} from 'components/FixCreationTime'; } from 'components/FixCreationTime';
import { Collection, CollectionSummaries } from 'types/collection'; import { Collection, CollectionSummaries } from 'types/collection';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { GalleryContextType, SelectedState, SetFiles } from 'types/gallery'; import { GalleryContextType, SelectedState } from 'types/gallery';
import { VISIBILITY_STATE } from 'types/magicMetadata'; import { VISIBILITY_STATE } from 'types/magicMetadata';
import Collections from 'components/Collections'; import Collections from 'components/Collections';
import { GalleryNavbar } from 'components/pages/gallery/Navbar'; import { GalleryNavbar } from 'components/pages/gallery/Navbar';
@ -124,60 +125,18 @@ export const GalleryContext = createContext<GalleryContextType>(
defaultGalleryContext defaultGalleryContext
); );
type FilesFn = EnteFile[] | ((files: EnteFile[]) => EnteFile[]);
export default function Gallery() { export default function Gallery() {
const router = useRouter(); const router = useRouter();
const [user, setUser] = useState(null); const [user, setUser] = useState(null);
const [collections, setCollections] = useState<Collection[]>(null); const [collections, setCollections] = useState<Collection[]>(null);
const [files, setFilesOriginal] = useState<EnteFile[]>(null); const [files, setFiles] = useState<EnteFile[]>(null);
const filesUpdateInProgress = useRef(false);
const filesCount = useRef(0);
const newerFilesFN = useRef<FilesFn>(null);
const setFilesOriginalWithReSyncIfRequired: SetFiles = (filesFn) => {
setFilesOriginal((currentFiles) => {
let newFiles: EnteFile[];
if (typeof filesFn === 'function') {
newFiles = filesFn(currentFiles);
} else {
newFiles = filesFn;
}
filesCount.current = newFiles?.length;
return newFiles;
});
filesUpdateInProgress.current = false;
if (newerFilesFN.current) {
const newerFiles = newerFilesFN.current;
setTimeout(() => setFiles(newerFiles), 0);
newerFilesFN.current = null;
}
};
const setFiles: SetFiles = async (filesFn) => {
if (filesUpdateInProgress.current) {
newerFilesFN.current = filesFn;
return;
}
filesUpdateInProgress.current = true;
if (!filesCount.current || filesCount.current < 5000) {
setFilesOriginalWithReSyncIfRequired(filesFn);
} else {
const waitTime = getData(LS_KEYS.WAIT_TIME) ?? 5000;
setTimeout(
() => setFilesOriginalWithReSyncIfRequired(filesFn),
waitTime
);
}
};
const [favItemIds, setFavItemIds] = useState<Set<number>>(); const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [isFirstLoad, setIsFirstLoad] = useState(false); const [isFirstLoad, setIsFirstLoad] = useState(false);
const [isFirstFetch, setIsFirstFetch] = useState(false); const [isFirstFetch, setIsFirstFetch] = useState(false);
const [selected, setSelected] = useState<SelectedState>({ const [selected, setSelected] = useState<SelectedState>({
ownCount: 0,
count: 0, count: 0,
collectionID: 0, collectionID: 0,
}); });
@ -326,6 +285,8 @@ export default function Gallery() {
collectionURL += constants.ARCHIVE; collectionURL += constants.ARCHIVE;
} else if (activeCollection === TRASH_SECTION) { } else if (activeCollection === TRASH_SECTION) {
collectionURL += constants.TRASH; collectionURL += constants.TRASH;
} else if (activeCollection === DUMMY_UNCATEGORIZED_SECTION) {
collectionURL += UNCATEGORIZED_COLLECTION_NAME;
} else { } else {
collectionURL += activeCollection; collectionURL += activeCollection;
} }
@ -373,10 +334,8 @@ export default function Gallery() {
!silent && startLoading(); !silent && startLoading();
const collections = await syncCollections(); const collections = await syncCollections();
setCollections(collections); setCollections(collections);
let files = await syncFiles(collections, setFiles); await syncFiles(collections, setFiles);
const trash = await syncTrash(collections, setFiles, files); await syncTrash(collections, setFiles);
files = [...files, ...getTrashedFiles(trash)];
setFiles(sortFiles(files));
} catch (e) { } catch (e) {
logError(e, 'syncWithRemote failed'); logError(e, 'syncWithRemote failed');
switch (e.message) { switch (e.message) {
@ -389,6 +348,7 @@ export default function Gallery() {
break; break;
} }
} finally { } finally {
setDeletedFileIds(new Set());
!silent && finishLoading(); !silent && finishLoading();
} }
syncInProgress.current = false; syncInProgress.current = false;
@ -408,7 +368,7 @@ export default function Gallery() {
const archivedCollections = getArchivedCollections(collections); const archivedCollections = getArchivedCollections(collections);
setArchivedCollections(archivedCollections); setArchivedCollections(archivedCollections);
const collectionSummaries = getCollectionSummaries( const collectionSummaries = await getCollectionSummaries(
user, user,
collections, collections,
files, files,
@ -418,7 +378,7 @@ export default function Gallery() {
}; };
const clearSelection = function () { const clearSelection = function () {
setSelected({ count: 0, collectionID: 0 }); setSelected({ ownCount: 0, count: 0, collectionID: 0 });
}; };
if (!files || !collectionSummaries) { if (!files || !collectionSummaries) {
@ -430,10 +390,19 @@ export default function Gallery() {
try { try {
setCollectionSelectorView(false); setCollectionSelectorView(false);
const selectedFiles = getSelectedFiles(selected, files); const selectedFiles = getSelectedFiles(selected, files);
const toProcessFiles =
ops === COLLECTION_OPS_TYPE.REMOVE
? selectedFiles
: selectedFiles.filter(
(file) => file.ownerID === user.id
);
if (toProcessFiles.length === 0) {
return;
}
await handleCollectionOps( await handleCollectionOps(
ops, ops,
collection, collection,
selectedFiles, toProcessFiles,
selected.collectionID selected.collectionID
); );
clearSelection(); clearSelection();
@ -551,7 +520,6 @@ export default function Gallery() {
}); });
} finally { } finally {
await syncWithRemote(false, true); await syncWithRemote(false, true);
setDeletedFileIds(new Set());
finishLoading(); finishLoading();
} }
}; };
@ -724,10 +692,10 @@ export default function Gallery() {
deletedFileIds={deletedFileIds} deletedFileIds={deletedFileIds}
setDeletedFileIds={setDeletedFileIds} setDeletedFileIds={setDeletedFileIds}
activeCollection={activeCollection} activeCollection={activeCollection}
isSharedCollection={isSharedCollection( isIncomingSharedCollection={
activeCollection, collectionSummaries.get(activeCollection)?.type ===
collections CollectionSummaryType.incomingShare
)} }
enableDownload={true} enableDownload={true}
resetSearch={resetSearch} resetSearch={resetSearch}
/> />
@ -771,12 +739,23 @@ export default function Gallery() {
fixTimeHelper={fixTimeHelper} fixTimeHelper={fixTimeHelper}
downloadHelper={downloadHelper} downloadHelper={downloadHelper}
count={selected.count} count={selected.count}
ownCount={selected.ownCount}
clearSelection={clearSelection} clearSelection={clearSelection}
activeCollection={activeCollection} activeCollection={activeCollection}
isFavoriteCollection={isFavoriteCollection( isFavoriteCollection={
activeCollection, collectionSummaries.get(activeCollection)
collections ?.type === CollectionSummaryType.favorites
)} }
isUncategorizedCollection={
collectionSummaries.get(activeCollection)
?.type ===
CollectionSummaryType.uncategorized
}
isIncomingSharedCollection={
collectionSummaries.get(activeCollection)
?.type ===
CollectionSummaryType.incomingShare
}
/> />
)} )}
</FullScreenDropZone> </FullScreenDropZone>

View file

@ -407,10 +407,10 @@ export default function PublicCollectionGallery() {
files={publicFiles} files={publicFiles}
syncWithRemote={syncWithRemote} syncWithRemote={syncWithRemote}
setSelected={() => null} setSelected={() => null}
selected={{ count: 0, collectionID: null }} selected={{ count: 0, collectionID: null, ownCount: 0 }}
isFirstLoad={true} isFirstLoad={true}
activeCollection={ALL_SECTION} activeCollection={ALL_SECTION}
isSharedCollection isIncomingSharedCollection
enableDownload={ enableDownload={
publicCollection?.publicURLs?.[0]?.enableDownload ?? publicCollection?.publicURLs?.[0]?.enableDownload ??
true true

View file

@ -19,7 +19,6 @@ import {
AddToCollectionRequest, AddToCollectionRequest,
MoveToCollectionRequest, MoveToCollectionRequest,
EncryptedFileKey, EncryptedFileKey,
RemoveFromCollectionRequest,
CreatePublicAccessTokenRequest, CreatePublicAccessTokenRequest,
PublicURL, PublicURL,
UpdatePublicURL, UpdatePublicURL,
@ -29,6 +28,7 @@ import {
EncryptedCollection, EncryptedCollection,
CollectionMagicMetadata, CollectionMagicMetadata,
CollectionMagicMetadataProps, CollectionMagicMetadataProps,
RemoveFromCollectionRequest,
} from 'types/collection'; } from 'types/collection';
import { import {
COLLECTION_SORT_BY, COLLECTION_SORT_BY,
@ -38,6 +38,9 @@ import {
COLLECTION_SORT_ORDER, COLLECTION_SORT_ORDER,
ALL_SECTION, ALL_SECTION,
CollectionSummaryType, CollectionSummaryType,
UNCATEGORIZED_COLLECTION_NAME,
FAVORITE_COLLECTION_NAME,
DUMMY_UNCATEGORIZED_SECTION,
} from 'constants/collection'; } from 'constants/collection';
import { import {
NEW_COLLECTION_MAGIC_METADATA, NEW_COLLECTION_MAGIC_METADATA,
@ -53,8 +56,10 @@ import {
isOutgoingShare, isOutgoingShare,
isIncomingShare, isIncomingShare,
isSharedOnlyViaLink, isSharedOnlyViaLink,
isValidMoveTarget,
} from 'utils/collection'; } from 'utils/collection';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker'; import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import { getLocalFiles } from './fileService';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const COLLECTION_TABLE = 'collections'; const COLLECTION_TABLE = 'collections';
@ -238,7 +243,8 @@ export const getCollection = async (
); );
return collectionWithSecrets; return collectionWithSecrets;
} catch (e) { } catch (e) {
logError(e, 'failed to get collection', { collectionID }); logError(e, 'failed to get collection');
throw e;
} }
}; };
@ -358,7 +364,7 @@ export const addToFavorites = async (file: EnteFile) => {
let favCollection = await getFavCollection(); let favCollection = await getFavCollection();
if (!favCollection) { if (!favCollection) {
favCollection = await createCollection( favCollection = await createCollection(
'Favorites', FAVORITE_COLLECTION_NAME,
CollectionType.favorites CollectionType.favorites
); );
const localCollections = await getLocalCollections(); const localCollections = await getLocalCollections();
@ -491,35 +497,142 @@ const encryptWithNewCollectionKey = async (
}; };
export const removeFromCollection = async ( export const removeFromCollection = async (
collectionID: number, collectionID: number,
files: EnteFile[] toRemoveFiles: EnteFile[],
allFiles?: EnteFile[]
) => { ) => {
try { try {
const token = getToken(); const user: User = getData(LS_KEYS.USER);
const request: RemoveFromCollectionRequest = { const nonUserFiles = [];
collectionID: collectionID, const userFiles = [];
fileIDs: files.map((file) => file.id), for (const file of toRemoveFiles) {
}; if (file.ownerID === user.id) {
userFiles.push(file);
} else {
nonUserFiles.push(file);
}
}
await HTTPService.post( if (nonUserFiles.length > 0) {
`${ENDPOINT}/collections/v2/remove-files`, await removeNonUserFiles(collectionID, nonUserFiles);
request, }
null, if (userFiles.length > 0) {
{ 'X-Auth-Token': token } await removeUserFiles(collectionID, userFiles, allFiles);
); }
} catch (e) { } catch (e) {
logError(e, 'remove from collection failed '); logError(e, 'remove from collection failed ');
throw e; throw e;
} }
}; };
export const deleteCollection = async (collectionID: number) => { export const removeUserFiles = async (
sourceCollectionID: number,
toRemoveFiles: EnteFile[],
allFiles?: EnteFile[]
) => {
try { try {
if (!allFiles) {
allFiles = await getLocalFiles();
}
const toRemoveFilesIds = new Set(toRemoveFiles.map((f) => f.id));
const toRemoveFilesCopiesInOtherCollections = allFiles.filter((f) => {
return toRemoveFilesIds.has(f.id);
});
const groupiedFiles = groupFilesBasedOnCollectionID(
toRemoveFilesCopiesInOtherCollections
);
const collections = await getLocalCollections();
const collectionsMap = new Map(collections.map((c) => [c.id, c]));
const user: User = getData(LS_KEYS.USER);
for (const [targetCollectionID, files] of groupiedFiles.entries()) {
const targetCollection = collectionsMap.get(targetCollectionID);
if (
!isValidMoveTarget(sourceCollectionID, targetCollection, user)
) {
continue;
}
const toMoveFiles = files.filter((f) => {
if (toRemoveFilesIds.has(f.id)) {
toRemoveFilesIds.delete(f.id);
return true;
}
return false;
});
if (toMoveFiles.length === 0) {
continue;
}
await moveToCollection(
targetCollection,
sourceCollectionID,
toMoveFiles
);
}
const leftFiles = toRemoveFiles.filter((f) =>
toRemoveFilesIds.has(f.id)
);
if (leftFiles.length === 0) {
return;
}
let uncategorizedCollection = await getUncategorizedCollection();
if (!uncategorizedCollection) {
uncategorizedCollection = await createUnCategorizedCollection();
}
await moveToCollection(
uncategorizedCollection,
sourceCollectionID,
leftFiles
);
} catch (e) {
logError(e, 'remove user files failed ');
throw e;
}
};
export const removeNonUserFiles = async (
collectionID: number,
nonUserFiles: EnteFile[]
) => {
try {
const fileIDs = nonUserFiles.map((f) => f.id);
const token = getToken();
const request: RemoveFromCollectionRequest = {
collectionID,
fileIDs,
};
await HTTPService.post(
`${ENDPOINT}/collections/v3/remove-files`,
request,
null,
{ 'X-Auth-Token': token }
);
} catch (e) {
logError(e, 'remove non user files failed ');
throw e;
}
};
export const deleteCollection = async (
collectionID: number,
keepFiles: boolean
) => {
try {
if (keepFiles) {
const allFiles = await getLocalFiles();
const collectionFiles = allFiles.filter((file) => {
return file.collectionID === collectionID;
});
await removeFromCollection(collectionID, collectionFiles, allFiles);
}
const token = getToken(); const token = getToken();
await HTTPService.delete( await HTTPService.delete(
`${ENDPOINT}/collections/v2/${collectionID}`, `${ENDPOINT}/collections/v3/${collectionID}`,
null,
null, null,
{ collectionID, keepFiles },
{ 'X-Auth-Token': token } { 'X-Auth-Token': token }
); );
} catch (e) { } catch (e) {
@ -816,12 +929,12 @@ function compareCollectionsLatestFile(first: EnteFile, second: EnteFile) {
} }
} }
export function getCollectionSummaries( export async function getCollectionSummaries(
user: User, user: User,
collections: Collection[], collections: Collection[],
files: EnteFile[], files: EnteFile[],
archivedCollections: Set<number> archivedCollections: Set<number>
): CollectionSummaries { ): Promise<CollectionSummaries> {
const collectionSummaries: CollectionSummaries = new Map(); const collectionSummaries: CollectionSummaries = new Map();
const collectionLatestFiles = getCollectionLatestFiles( const collectionLatestFiles = getCollectionLatestFiles(
files, files,
@ -833,12 +946,15 @@ export function getCollectionSummaries(
); );
for (const collection of collections) { for (const collection of collections) {
if (collectionFilesCount.get(collection.id)) { if (
collectionFilesCount.get(collection.id) ||
collection.type === CollectionType.uncategorized
) {
collectionSummaries.set(collection.id, { collectionSummaries.set(collection.id, {
id: collection.id, id: collection.id,
name: collection.name, name: collection.name,
latestFile: collectionLatestFiles.get(collection.id), latestFile: collectionLatestFiles.get(collection.id),
fileCount: collectionFilesCount.get(collection.id), fileCount: collectionFilesCount.get(collection.id) ?? 0,
updationTime: collection.updationTime, updationTime: collection.updationTime,
type: isIncomingShare(collection, user) type: isIncomingShare(collection, user)
? CollectionSummaryType.incomingShare ? CollectionSummaryType.incomingShare
@ -852,6 +968,16 @@ export function getCollectionSummaries(
}); });
} }
} }
const uncategorizedCollection = await getUncategorizedCollection(
collections
);
if (!uncategorizedCollection) {
collectionSummaries.set(
DUMMY_UNCATEGORIZED_SECTION,
getDummyUncategorizedCollectionSummaries()
);
}
collectionSummaries.set( collectionSummaries.set(
ALL_SECTION, ALL_SECTION,
getAllCollectionSummaries(collectionFilesCount, collectionLatestFiles) getAllCollectionSummaries(collectionFilesCount, collectionLatestFiles)
@ -919,6 +1045,17 @@ function getAllCollectionSummaries(
}; };
} }
function getDummyUncategorizedCollectionSummaries(): CollectionSummary {
return {
id: ALL_SECTION,
name: UNCATEGORIZED_COLLECTION_NAME,
type: CollectionSummaryType.uncategorized,
latestFile: null,
fileCount: 0,
updationTime: 0,
};
}
function getArchivedCollectionSummaries( function getArchivedCollectionSummaries(
collectionFilesCount: CollectionFilesCount, collectionFilesCount: CollectionFilesCount,
collectionsLatestFile: CollectionLatestFiles collectionsLatestFile: CollectionLatestFiles
@ -946,3 +1083,23 @@ function getTrashedCollectionSummaries(
updationTime: collectionsLatestFile.get(TRASH_SECTION)?.updationTime, updationTime: collectionsLatestFile.get(TRASH_SECTION)?.updationTime,
}; };
} }
export async function getUncategorizedCollection(
collections?: Collection[]
): Promise<Collection> {
if (!collections) {
collections = await getLocalCollections();
}
const uncategorizedCollection = collections.find(
(collection) => collection.type === CollectionType.uncategorized
);
return uncategorizedCollection;
}
export async function createUnCategorizedCollection() {
return createCollection(
UNCATEGORIZED_COLLECTION_NAME,
CollectionType.uncategorized
);
}

View file

@ -1,5 +1,6 @@
import { ElectronAPIs } from 'types/electron'; import { ElectronAPIs } from 'types/electron';
import { ElectronFile } from 'types/upload'; import { ElectronFile } from 'types/upload';
import { CustomError } from 'utils/error';
import { convertBytesToHumanReadable } from 'utils/file/size'; import { convertBytesToHumanReadable } from 'utils/file/size';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -37,7 +38,12 @@ class ElectronImageProcessorService {
); );
return new Blob([convertedFileData]); return new Blob([convertedFileData]);
} catch (e) { } catch (e) {
logError(e, 'failed to convert heic natively'); if (
e.message !==
CustomError.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED
) {
logError(e, 'failed to convert heic natively');
}
throw e; throw e;
} }
} }
@ -68,7 +74,12 @@ class ElectronImageProcessorService {
); );
return thumb; return thumb;
} catch (e) { } catch (e) {
logError(e, 'failed to generate image thumbnail natively'); if (
e.message !==
CustomError.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED
) {
logError(e, 'failed to generate image thumbnail natively');
}
throw e; throw e;
} }
} }

View file

@ -7,8 +7,8 @@ import HTTPService from './HTTPService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { import {
decryptFile, decryptFile,
getLatestVersionFiles,
mergeMetadata, mergeMetadata,
preservePhotoswipeProps,
sortFiles, sortFiles,
} from 'utils/file'; } from 'utils/file';
import { EnteFile, EncryptedEnteFile, TrashRequest } from 'types/file'; import { EnteFile, EncryptedEnteFile, TrashRequest } from 'types/file';
@ -54,12 +54,12 @@ const setLocalFiles = async (files: EnteFile[]) => {
export const syncFiles = async ( export const syncFiles = async (
collections: Collection[], collections: Collection[],
setFiles: SetFiles setFiles: SetFiles
) => { ): Promise<EnteFile[]> => {
const localFiles = await getLocalFiles(); const localFiles = await getLocalFiles();
let files = await removeDeletedCollectionFiles(collections, localFiles); let files = await removeDeletedCollectionFiles(collections, localFiles);
if (files.length !== localFiles.length) { if (files.length !== localFiles.length) {
await setLocalFiles(files); await setLocalFiles(files);
setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))])); setFiles(sortFiles(mergeMetadata(files)));
} }
for (const collection of collections) { for (const collection of collections) {
if (!getToken()) { if (!getToken()) {
@ -72,38 +72,17 @@ export const syncFiles = async (
if (collection.updationTime === lastSyncTime) { if (collection.updationTime === lastSyncTime) {
continue; continue;
} }
const fetchedFiles = const newFiles = await getFiles(collection, lastSyncTime, setFiles);
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? []; files = getLatestVersionFiles([...files, ...newFiles]);
files = [...files, ...fetchedFiles];
const latestVersionFiles = new Map<string, EnteFile>();
files.forEach((file) => {
const uid = `${file.collectionID}-${file.id}`;
if (
!latestVersionFiles.has(uid) ||
latestVersionFiles.get(uid).updationTime < file.updationTime
) {
latestVersionFiles.set(uid, file);
}
});
files = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_, file] of latestVersionFiles) {
if (file.isDeleted) {
continue;
}
files.push(file);
}
await setLocalFiles(files); await setLocalFiles(files);
setCollectionLastSyncTime(collection, collection.updationTime); setCollectionLastSyncTime(collection, collection.updationTime);
setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))]));
} }
return sortFiles(mergeMetadata(files)); return files;
}; };
export const getFiles = async ( export const getFiles = async (
collection: Collection, collection: Collection,
sinceTime: number, sinceTime: number,
files: EnteFile[],
setFiles: SetFiles setFiles: SetFiles
): Promise<EnteFile[]> => { ): Promise<EnteFile[]> => {
try { try {
@ -126,37 +105,35 @@ export const getFiles = async (
} }
); );
decryptedFiles = [ const newDecryptedFilesBatch = await Promise.all(
...decryptedFiles, resp.data.diff.map(async (file: EncryptedEnteFile) => {
...(await Promise.all( if (!file.isDeleted) {
resp.data.diff.map(async (file: EncryptedEnteFile) => { return await decryptFile(file, collection.key);
if (!file.isDeleted) { } else {
return await decryptFile(file, collection.key); return file;
} else { }
return file; }) as Promise<EnteFile>[]
} );
}) as Promise<EnteFile>[] decryptedFiles = [...decryptedFiles, ...newDecryptedFilesBatch];
)),
];
if (resp.data.diff.length) { setFiles((files) =>
time = resp.data.diff.slice(-1)[0].updationTime; sortFiles(
} mergeMetadata(
setFiles( getLatestVersionFiles([
preservePhotoswipeProps( ...(files || []),
sortFiles( ...decryptedFiles,
mergeMetadata( ])
[...(files || []), ...decryptedFiles].filter(
(item) => !item.isDeleted
)
)
) )
) )
); );
if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime;
}
} while (resp.data.hasMore); } while (resp.data.hasMore);
return decryptedFiles; return decryptedFiles;
} catch (e) { } catch (e) {
logError(e, 'Get files failed'); logError(e, 'Get files failed');
throw e;
} }
}; };

View file

@ -2,16 +2,10 @@ import { SetFiles } from 'types/gallery';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import { import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
decryptFile,
mergeMetadata,
preservePhotoswipeProps,
sortFiles,
} from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import { getCollection } from './collectionService'; import { getCollection } from './collectionService';
import { EnteFile } from 'types/file';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { EncryptedTrashItem, Trash } from 'types/trash'; import { EncryptedTrashItem, Trash } from 'types/trash';
@ -30,7 +24,13 @@ export async function getLocalTrash() {
export async function getLocalDeletedCollections() { export async function getLocalDeletedCollections() {
const trashedCollections: Array<Collection> = const trashedCollections: Array<Collection> =
(await localForage.getItem<Collection[]>(DELETED_COLLECTION)) || []; (await localForage.getItem<Collection[]>(DELETED_COLLECTION)) || [];
return trashedCollections; const nonUndefinedCollections = trashedCollections.filter(
(collection) => !!collection
);
if (nonUndefinedCollections.length !== trashedCollections.length) {
await localForage.setItem(DELETED_COLLECTION, nonUndefinedCollections);
}
return nonUndefinedCollections;
} }
export async function cleanTrashCollections(fileTrash: Trash) { export async function cleanTrashCollections(fileTrash: Trash) {
@ -49,16 +49,15 @@ async function getLastSyncTime() {
} }
export async function syncTrash( export async function syncTrash(
collections: Collection[], collections: Collection[],
setFiles: SetFiles, setFiles: SetFiles
files: EnteFile[] ): Promise<void> {
): Promise<Trash> {
const trash = await getLocalTrash(); const trash = await getLocalTrash();
collections = [...collections, ...(await getLocalDeletedCollections())]; collections = [...collections, ...(await getLocalDeletedCollections())];
const collectionMap = new Map<number, Collection>( const collectionMap = new Map<number, Collection>(
collections.map((collection) => [collection.id, collection]) collections.map((collection) => [collection.id, collection])
); );
if (!getToken()) { if (!getToken()) {
return trash; return;
} }
const lastSyncTime = await getLastSyncTime(); const lastSyncTime = await getLastSyncTime();
@ -66,18 +65,15 @@ export async function syncTrash(
collectionMap, collectionMap,
lastSyncTime, lastSyncTime,
setFiles, setFiles,
files,
trash trash
); );
cleanTrashCollections(updatedTrash); cleanTrashCollections(updatedTrash);
return updatedTrash;
} }
export const updateTrash = async ( export const updateTrash = async (
collections: Map<number, Collection>, collections: Map<number, Collection>,
sinceTime: number, sinceTime: number,
setFiles: SetFiles, setFiles: SetFiles,
files: EnteFile[],
currentTrash: Trash currentTrash: Trash
): Promise<Trash> => { ): Promise<Trash> => {
try { try {
@ -127,16 +123,8 @@ export const updateTrash = async (
time = resp.data.diff.slice(-1)[0].updatedAt; time = resp.data.diff.slice(-1)[0].updatedAt;
} }
setFiles( setFiles((files) =>
preservePhotoswipeProps( sortFiles([...(files ?? []), ...getTrashedFiles(updatedTrash)])
sortFiles([
...(files ?? []).map((file) => ({
...file,
isTrashed: false,
})),
...getTrashedFiles(updatedTrash),
])
)
); );
await localForage.setItem(TRASH, updatedTrash); await localForage.setItem(TRASH, updatedTrash);
await localForage.setItem(TRASH_TIME, time); await localForage.setItem(TRASH_TIME, time);

View file

@ -1,6 +1,9 @@
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { ElectronFile, FileTypeInfo } from 'types/upload'; import { ElectronFile, FileTypeInfo } from 'types/upload';
import { FILE_TYPE_LIB_MISSED_FORMATS } from 'constants/upload'; import {
FILE_TYPE_LIB_MISSED_FORMATS,
KNOWN_NON_MEDIA_FORMATS,
} from 'constants/upload';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { getFileExtension } from 'utils/file'; import { getFileExtension } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -27,7 +30,7 @@ export async function getFileType(
const mimTypeParts: string[] = typeResult.mime?.split('/'); const mimTypeParts: string[] = typeResult.mime?.split('/');
if (mimTypeParts?.length !== 2) { if (mimTypeParts?.length !== 2) {
throw Error(CustomError.TYPE_DETECTION_FAILED); throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
} }
switch (mimTypeParts[0]) { switch (mimTypeParts[0]) {
case TYPE_IMAGE: case TYPE_IMAGE:
@ -37,7 +40,7 @@ export async function getFileType(
fileType = FILE_TYPE.VIDEO; fileType = FILE_TYPE.VIDEO;
break; break;
default: default:
fileType = FILE_TYPE.OTHERS; throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
} }
return { return {
fileType, fileType,
@ -45,6 +48,9 @@ export async function getFileType(
mimeType: typeResult.mime, mimeType: typeResult.mime,
}; };
} catch (e) { } catch (e) {
if (e.message === CustomError.UNSUPPORTED_FILE_FORMAT) {
throw e;
}
const fileFormat = getFileExtension(receivedFile.name); const fileFormat = getFileExtension(receivedFile.name);
const formatMissedByTypeDetection = FILE_TYPE_LIB_MISSED_FORMATS.find( const formatMissedByTypeDetection = FILE_TYPE_LIB_MISSED_FORMATS.find(
(a) => a.exactType === fileFormat (a) => a.exactType === fileFormat
@ -52,14 +58,13 @@ export async function getFileType(
if (formatMissedByTypeDetection) { if (formatMissedByTypeDetection) {
return formatMissedByTypeDetection; return formatMissedByTypeDetection;
} }
logError(e, CustomError.TYPE_DETECTION_FAILED, { if (KNOWN_NON_MEDIA_FORMATS.includes(fileFormat)) {
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
}
logError(e, 'type detection failed', {
fileFormat, fileFormat,
}); });
return { throw Error(CustomError.TYPE_DETECTION_FAILED(fileFormat));
fileType: FILE_TYPE.OTHERS,
exactType: fileFormat,
mimeType: receivedFile instanceof File ? receivedFile.type : null,
};
} }
} }
@ -78,18 +83,15 @@ async function extractElectronFileType(file: ElectronFile) {
} }
async function getFileTypeFromBuffer(buffer: Uint8Array) { async function getFileTypeFromBuffer(buffer: Uint8Array) {
try { const result = await FileType.fromBuffer(buffer);
const result = await FileType.fromBuffer(buffer); if (!result?.mime) {
if (!result.mime) { let logableInfo = '';
logError( try {
Error('mimetype missing from file type result'), logableInfo = `result: ${JSON.stringify(result)}`;
CustomError.TYPE_DETECTION_FAILED, } catch (e) {
{ result } logableInfo = 'failed to stringify result';
);
throw Error(CustomError.TYPE_DETECTION_FAILED);
} }
return result; throw Error(`mimetype missing from file type result - ${logableInfo}`);
} catch (e) {
throw Error(CustomError.TYPE_DETECTION_FAILED);
} }
return result;
} }

View file

@ -88,13 +88,6 @@ async function generateImageThumbnail(
MAX_THUMBNAIL_SIZE MAX_THUMBNAIL_SIZE
); );
} catch (e) { } catch (e) {
logError(
e,
'Error generating thumbnail using electron image processor',
{
fileFormat: fileTypeInfo.exactType,
}
);
return await generateImageThumbnailUsingCanvas(file, fileTypeInfo); return await generateImageThumbnailUsingCanvas(file, fileTypeInfo);
} }
} else { } else {

View file

@ -2,7 +2,6 @@ import { getLocalFiles } from '../fileService';
import { SetFiles } from 'types/gallery'; import { SetFiles } from 'types/gallery';
import { import {
sortFiles, sortFiles,
preservePhotoswipeProps,
decryptFile, decryptFile,
getUserOwnedNonTrashedFiles, getUserOwnedNonTrashedFiles,
} from 'utils/file'; } from 'utils/file';
@ -414,7 +413,7 @@ class UploadManager {
private updateUIFiles(decryptedFile: EnteFile) { private updateUIFiles(decryptedFile: EnteFile) {
this.existingFiles.push(decryptedFile); this.existingFiles.push(decryptedFile);
this.existingFiles = sortFiles(this.existingFiles); this.existingFiles = sortFiles(this.existingFiles);
this.setFiles(preservePhotoswipeProps(this.existingFiles)); this.setFiles((files) => sortFiles([...files, decryptedFile]));
} }
private updateElectronRemainingFiles( private updateElectronRemainingFiles(

View file

@ -51,10 +51,9 @@ export default async function uploader(
} }
addLogLine(`getting filetype for ${fileNameSize}`); addLogLine(`getting filetype for ${fileNameSize}`);
fileTypeInfo = await UploadService.getAssetFileType(uploadAsset); fileTypeInfo = await UploadService.getAssetFileType(uploadAsset);
addLogLine(`got filetype for ${fileNameSize}`); addLogLine(
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) { `got filetype for ${fileNameSize} - ${JSON.stringify(fileTypeInfo)}`
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT); );
}
if (skipVideos && fileTypeInfo.fileType === FILE_TYPE.VIDEO) { if (skipVideos && fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
addLogLine( addLogLine(
`skipped video upload for public upload ${fileNameSize}` `skipped video upload for public upload ${fileNameSize}`
@ -176,7 +175,10 @@ export default async function uploader(
}; };
} catch (e) { } catch (e) {
addLogLine(`upload failed for ${fileNameSize} ,error: ${e.message}`); addLogLine(`upload failed for ${fileNameSize} ,error: ${e.message}`);
if (e.message !== CustomError.UPLOAD_CANCELLED) { if (
e.message !== CustomError.UPLOAD_CANCELLED &&
e.message !== CustomError.UNSUPPORTED_FILE_FORMAT
) {
logError(e, 'file upload failed', { logError(e, 'file upload failed', {
fileFormat: fileTypeInfo?.exactType, fileFormat: fileTypeInfo?.exactType,
}); });

View file

@ -18,12 +18,12 @@ import {
UserDetails, UserDetails,
DeleteChallengeResponse, DeleteChallengeResponse,
} from 'types/user'; } from 'types/user';
import { getLocalFamilyData, isPartOfFamily } from 'utils/billing';
import { ServerErrorCodes } from 'utils/error'; import { ServerErrorCodes } from 'utils/error';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import safeStorageService from './electron/safeStorage'; import safeStorageService from './electron/safeStorage';
import { deleteThumbnailCache } from './cacheService'; import { deleteThumbnailCache } from './cacheService';
import { B64EncryptionResult } from 'types/crypto'; import { B64EncryptionResult } from 'types/crypto';
import { isPartOfFamily, getLocalFamilyData } from 'utils/user/family';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();

View file

@ -37,7 +37,6 @@ export interface Collection
> { > {
key: string; key: string;
name: string; name: string;
isSharedCollection?: boolean;
magicMetadata: CollectionMagicMetadata; magicMetadata: CollectionMagicMetadata;
} }

View file

@ -17,6 +17,12 @@ export interface DialogBoxAttributes {
variant: ButtonProps['color']; variant: ButtonProps['color'];
disabled?: boolean; disabled?: boolean;
}; };
secondary?: {
text: string;
action: () => void;
variant: ButtonProps['color'];
disabled?: boolean;
};
} }
export type SetDialogBoxAttributes = React.Dispatch< export type SetDialogBoxAttributes = React.Dispatch<

View file

@ -5,6 +5,7 @@ import { EnteFile } from 'types/file';
export type SelectedState = { export type SelectedState = {
[k: number]: boolean; [k: number]: boolean;
ownCount: number;
count: number; count: number;
collectionID: number; collectionID: number;
}; };
@ -15,9 +16,14 @@ export type SetCollectionSelectorAttributes = React.Dispatch<
React.SetStateAction<CollectionSelectorAttributes> React.SetStateAction<CollectionSelectorAttributes>
>; >;
export type MergedSourceURL = {
original: string;
converted: string;
};
export type GalleryContextType = { export type GalleryContextType = {
thumbs: Map<number, string>; thumbs: Map<number, string>;
files: Map<number, { original: string; converted: string }>; files: Map<number, MergedSourceURL>;
showPlanSelectorModal: () => void; showPlanSelectorModal: () => void;
setActiveCollection: (collection: number) => void; setActiveCollection: (collection: number) => void;
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>; syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;

View file

@ -8,8 +8,9 @@ import { CustomError } from '../error';
import { logError } from '../sentry'; import { logError } from '../sentry';
import { SetDialogBoxAttributes } from 'types/dialogBox'; import { SetDialogBoxAttributes } from 'types/dialogBox';
import { getFamilyPortalRedirectURL } from 'services/userService'; import { getFamilyPortalRedirectURL } from 'services/userService';
import { FamilyData, FamilyMember, User, UserDetails } from 'types/user';
import { openLink } from 'utils/common'; import { openLink } from 'utils/common';
import { isPartOfFamily, getTotalFamilyUsage } from 'utils/user/family';
import { UserDetails } from 'types/user';
const PAYMENT_PROVIDER_STRIPE = 'stripe'; const PAYMENT_PROVIDER_STRIPE = 'stripe';
const PAYMENT_PROVIDER_APPSTORE = 'appstore'; const PAYMENT_PROVIDER_APPSTORE = 'appstore';
@ -97,52 +98,10 @@ export function isSubscriptionCancelled(subscription: Subscription) {
return subscription && subscription.attributes.isCancelled; return subscription && subscription.attributes.isCancelled;
} }
// isPartOfFamily return true if the current user is part of some family plan
export function isPartOfFamily(familyData: FamilyData): boolean {
return Boolean(
familyData && familyData.members && familyData.members.length > 0
);
}
// hasNonAdminFamilyMembers return true if the admin user has members in his family
export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean {
return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1);
}
export function isFamilyAdmin(familyData: FamilyData): boolean {
const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData);
const user: User = getData(LS_KEYS.USER);
return familyAdmin.email === user.email;
}
export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember {
if (isPartOfFamily(familyData)) {
return familyData.members.find((x) => x.isAdmin);
} else {
logError(
Error(
'verify user is part of family plan before calling this method'
),
'invalid getFamilyPlanAdmin call'
);
}
}
export function getTotalFamilyUsage(familyData: FamilyData): number {
return familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0
);
}
export function getLocalUserSubscription(): Subscription { export function getLocalUserSubscription(): Subscription {
return getData(LS_KEYS.SUBSCRIPTION); return getData(LS_KEYS.SUBSCRIPTION);
} }
export function getLocalFamilyData(): FamilyData {
return getData(LS_KEYS.FAMILY_DATA);
}
export function isUserSubscribedPlan(plan: Plan, subscription: Subscription) { export function isUserSubscribedPlan(plan: Plan, subscription: Subscription) {
return ( return (
isSubscriptionActive(subscription) && isSubscriptionActive(subscription) &&

View file

@ -20,7 +20,6 @@ import {
} from 'types/collection'; } from 'types/collection';
import { import {
CollectionSummaryType, CollectionSummaryType,
CollectionType,
HIDE_FROM_COLLECTION_BAR_TYPES, HIDE_FROM_COLLECTION_BAR_TYPES,
OPTIONS_NOT_HAVING_COLLECTION_TYPES, OPTIONS_NOT_HAVING_COLLECTION_TYPES,
SYSTEM_COLLECTION_TYPES, SYSTEM_COLLECTION_TYPES,
@ -77,31 +76,6 @@ export function getSelectedCollection(
return collections.find((collection) => collection.id === collectionID); return collections.find((collection) => collection.id === collectionID);
} }
export function isSharedCollection(
collectionID: number,
collections: Collection[]
) {
const user: User = getData(LS_KEYS.USER);
const collection = getSelectedCollection(collectionID, collections);
if (!collection) {
return false;
}
return collection?.owner.id !== user.id;
}
export function isFavoriteCollection(
collectionID: number,
collections: Collection[]
) {
const collection = getSelectedCollection(collectionID, collections);
if (!collection) {
return false;
} else {
return collection.type === CollectionType.favorites;
}
}
export async function downloadAllCollectionFiles(collectionID: number) { export async function downloadAllCollectionFiles(collectionID: number) {
try { try {
const allFiles = await getLocalFiles(); const allFiles = await getLocalFiles();
@ -204,7 +178,10 @@ export const getArchivedCollections = (collections: Collection[]) => {
export const hasNonSystemCollections = ( export const hasNonSystemCollections = (
collectionSummaries: CollectionSummaries collectionSummaries: CollectionSummaries
) => { ) => {
return collectionSummaries?.size > 3; for (const collectionSummary of collectionSummaries.values()) {
if (!isSystemCollection(collectionSummary.type)) return true;
}
return false;
}; };
export const isUploadAllowedCollection = (type: CollectionSummaryType) => { export const isUploadAllowedCollection = (type: CollectionSummaryType) => {
@ -251,3 +228,16 @@ export function isIncomingShare(collection: Collection, user: User) {
export function isSharedOnlyViaLink(collection: Collection) { export function isSharedOnlyViaLink(collection: Collection) {
return collection.publicURLs?.length && !collection.sharees?.length; return collection.publicURLs?.length && !collection.sharees?.length;
} }
export function isValidMoveTarget(
sourceCollectionID: number,
targetCollection: Collection,
user: User
) {
return (
sourceCollectionID !== targetCollection.id &&
!isCollectionHidden(targetCollection) &&
!isQuickLinkCollection(targetCollection) &&
!isIncomingShare(targetCollection, user)
);
}

View file

@ -11,44 +11,49 @@ export const ServerErrorCodes = {
NOT_FOUND: '404', NOT_FOUND: '404',
}; };
export enum CustomError { export const CustomError = {
SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed', SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed',
THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed', THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
VIDEO_PLAYBACK_FAILED = 'video playback failed', VIDEO_PLAYBACK_FAILED: 'video playback failed',
ETAG_MISSING = 'no header/etag present in response body', ETAG_MISSING: 'no header/etag present in response body',
KEY_MISSING = 'encrypted key missing from localStorage', KEY_MISSING: 'encrypted key missing from localStorage',
FAILED_TO_LOAD_WEB_WORKER = 'failed to load web worker', FAILED_TO_LOAD_WEB_WORKER: 'failed to load web worker',
CHUNK_MORE_THAN_EXPECTED = 'chunks more than expected', CHUNK_MORE_THAN_EXPECTED: 'chunks more than expected',
CHUNK_LESS_THAN_EXPECTED = 'chunks less than expected', CHUNK_LESS_THAN_EXPECTED: 'chunks less than expected',
UNSUPPORTED_FILE_FORMAT = 'unsupported file formats', UNSUPPORTED_FILE_FORMAT: 'unsupported file format',
FILE_TOO_LARGE = 'file too large', FILE_TOO_LARGE: 'file too large',
SUBSCRIPTION_EXPIRED = 'subscription expired', SUBSCRIPTION_EXPIRED: 'subscription expired',
STORAGE_QUOTA_EXCEEDED = 'storage quota exceeded', STORAGE_QUOTA_EXCEEDED: 'storage quota exceeded',
SESSION_EXPIRED = 'session expired', SESSION_EXPIRED: 'session expired',
TYPE_DETECTION_FAILED = 'type detection failed', INVALID_MIME_TYPE: (type: string) => `invalid mime type -${type}`,
SIGNUP_FAILED = 'signup failed', SIGNUP_FAILED: 'signup failed',
FAV_COLLECTION_MISSING = 'favorite collection missing', FAV_COLLECTION_MISSING: 'favorite collection missing',
INVALID_COLLECTION_OPERATION = 'invalid collection operation', INVALID_COLLECTION_OPERATION: 'invalid collection operation',
WAIT_TIME_EXCEEDED = 'thumbnail generation wait time exceeded', WAIT_TIME_EXCEEDED: 'thumbnail generation wait time exceeded',
REQUEST_CANCELLED = 'request canceled', REQUEST_CANCELLED: 'request canceled',
REQUEST_FAILED = 'request failed', REQUEST_FAILED: 'request failed',
TOKEN_EXPIRED = 'token expired', TOKEN_EXPIRED: 'token expired',
TOKEN_MISSING = 'token missing', TOKEN_MISSING: 'token missing',
TOO_MANY_REQUESTS = 'too many requests', TOO_MANY_REQUESTS: 'too many requests',
BAD_REQUEST = 'bad request', BAD_REQUEST: 'bad request',
SUBSCRIPTION_NEEDED = 'subscription not present', SUBSCRIPTION_NEEDED: 'subscription not present',
NOT_FOUND = 'not found ', NOT_FOUND: 'not found ',
NO_METADATA = 'no metadata', NO_METADATA: 'no metadata',
TOO_LARGE_LIVE_PHOTO_ASSETS = 'too large live photo assets', TOO_LARGE_LIVE_PHOTO_ASSETS: 'too large live photo assets',
NOT_A_DATE = 'not a date', NOT_A_DATE: 'not a date',
FILE_ID_NOT_FOUND = 'file with id not found', FILE_ID_NOT_FOUND: 'file with id not found',
WEAK_DEVICE = 'password decryption failed on the device', WEAK_DEVICE: 'password decryption failed on the device',
INCORRECT_PASSWORD = 'incorrect password', INCORRECT_PASSWORD: 'incorrect password',
UPLOAD_CANCELLED = 'upload cancelled', UPLOAD_CANCELLED: 'upload cancelled',
REQUEST_TIMEOUT = 'request taking too long', REQUEST_TIMEOUT: 'request taking too long',
HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED = 'hidden collection sync file attempted', HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED:
UNKNOWN_ERROR = 'Something went wrong, please try again', 'hidden collection sync file attempted',
} UNKNOWN_ERROR: 'Something went wrong, please try again',
TYPE_DETECTION_FAILED: (fileFormat: string) =>
`type detection failed ${fileFormat}`,
WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED:
'Windows native image processing is not supported',
};
function parseUploadErrorCodes(error) { function parseUploadErrorCodes(error) {
let parsedMessage = null; let parsedMessage = null;

View file

@ -166,31 +166,14 @@ export function getSelectedFiles(
} }
export function sortFiles(files: EnteFile[]) { export function sortFiles(files: EnteFile[]) {
// sort according to modification time first // sort based on the time of creation time of the file,
files = files.sort((a, b) => { // for files with same creation time, sort based on the time of last modification
if (!b.metadata?.modificationTime) { return files.sort((a, b) => {
return -1; if (a.metadata.creationTime === b.metadata.creationTime) {
}
if (!a.metadata?.modificationTime) {
return 1;
} else {
return b.metadata.modificationTime - a.metadata.modificationTime; return b.metadata.modificationTime - a.metadata.modificationTime;
} }
return b.metadata.creationTime - a.metadata.creationTime;
}); });
// then sort according to creation time, maintaining ordering according to modification time for files with creation time
files = files
.map((file, index) => ({ index, file }))
.sort((a, b) => {
let diff =
b.file.metadata.creationTime - a.file.metadata.creationTime;
if (diff === 0) {
diff = a.index - b.index;
}
return diff;
})
.map((file) => file.file);
return files;
} }
export async function decryptFile( export async function decryptFile(
@ -252,19 +235,6 @@ export async function decryptFile(
} }
} }
export const preservePhotoswipeProps =
(newFiles: EnteFile[]) =>
(currentFiles: EnteFile[]): EnteFile[] => {
const currentFilesMap = Object.fromEntries(
currentFiles.map((file) => [file.id, file])
);
const fileWithPreservedProperty = newFiles.map((file) => {
const currentFile = currentFilesMap[file.id];
return { ...currentFile, ...file };
});
return fileWithPreservedProperty;
};
export function fileNameWithoutExtension(filename: string) { export function fileNameWithoutExtension(filename: string) {
const lastDotPosition = filename.lastIndexOf('.'); const lastDotPosition = filename.lastIndexOf('.');
if (lastDotPosition === -1) return filename; if (lastDotPosition === -1) return filename;
@ -456,23 +426,16 @@ export function isSharedFile(user: User, file: EnteFile) {
} }
export function mergeMetadata(files: EnteFile[]): EnteFile[] { export function mergeMetadata(files: EnteFile[]): EnteFile[] {
return files.map((file) => ({ return files.map((file) => {
...file, if (file.pubMagicMetadata?.data.editedTime) {
metadata: { file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
...file.metadata, }
...(file.pubMagicMetadata?.data if (file.pubMagicMetadata?.data.editedName) {
? { file.metadata.title = file.pubMagicMetadata.data.editedName;
...(file.pubMagicMetadata?.data.editedTime && { }
creationTime: file.pubMagicMetadata.data.editedTime,
}), return file;
...(file.pubMagicMetadata?.data.editedName && { });
title: file.pubMagicMetadata.data.editedName,
}),
}
: {}),
...(file.magicMetadata?.data ? file.magicMetadata.data : {}),
},
}));
} }
export function updateExistingFilePubMetadata( export function updateExistingFilePubMetadata(
@ -592,3 +555,19 @@ export const copyFileToClipboard = async (fileUrl: string) => {
.write([new ClipboardItem({ 'image/png': blobPromise })]) .write([new ClipboardItem({ 'image/png': blobPromise })])
.catch((e) => logError(e, 'failed to copy to clipboard')); .catch((e) => logError(e, 'failed to copy to clipboard'));
}; };
export function getLatestVersionFiles(files: EnteFile[]) {
const latestVersionFiles = new Map<string, EnteFile>();
files.forEach((file) => {
const uid = `${file.collectionID}-${file.id}`;
if (
!latestVersionFiles.has(uid) ||
latestVersionFiles.get(uid).updationTime < file.updationTime
) {
latestVersionFiles.set(uid, file);
}
});
return Array.from(latestVersionFiles.values()).filter(
(file) => !file.isDeleted
);
}

View file

@ -1,3 +1,9 @@
import { FILE_TYPE } from 'constants/file';
import { EnteFile } from 'types/file';
import { MergedSourceURL } from 'types/gallery';
import { logError } from 'utils/sentry';
import constants from 'utils/strings/constants';
const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000; const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000;
export async function isPlaybackPossible(url: string): Promise<boolean> { export async function isPlaybackPossible(url: string): Promise<boolean> {
@ -32,3 +38,121 @@ export async function pauseVideo(livePhotoVideo, livePhotoImage) {
livePhotoVideo.style.opacity = 0; livePhotoVideo.style.opacity = 0;
livePhotoImage.style.opacity = 1; livePhotoImage.style.opacity = 1;
} }
export function updateFileMsrcProps(file: EnteFile, url: string) {
file.msrc = url;
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
file.html = `
<div class="pswp-item-container">
<img src="${url}" onContextMenu="return false;"/>
<div class="spinner-border text-light" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
`;
} else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
file.html = `
<div class="pswp-item-container">
<img src="${url}" onContextMenu="return false;"/>
<div class="spinner-border text-light" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
`;
} else if (file.metadata.fileType === FILE_TYPE.IMAGE) {
file.src = url;
} else {
logError(
Error(`unknown file type - ${file.metadata.fileType}`),
'Unknown file type'
);
file.src = url;
}
}
export async function updateFileSrcProps(
file: EnteFile,
mergedURL: MergedSourceURL
) {
const urls = {
original: mergedURL.original.split(','),
converted: mergedURL.converted.split(','),
};
let originalImageURL;
let originalVideoURL;
let convertedImageURL;
let convertedVideoURL;
let originalURL;
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
[originalImageURL, originalVideoURL] = urls.original;
[convertedImageURL, convertedVideoURL] = urls.converted;
} else if (file.metadata.fileType === FILE_TYPE.VIDEO) {
[originalVideoURL] = urls.original;
[convertedVideoURL] = urls.converted;
} else if (file.metadata.fileType === FILE_TYPE.IMAGE) {
[originalImageURL] = urls.original;
[convertedImageURL] = urls.converted;
} else {
[originalURL] = urls.original;
}
const isPlayable =
convertedVideoURL && (await isPlaybackPossible(convertedVideoURL));
file.w = window.innerWidth;
file.h = window.innerHeight;
file.isSourceLoaded = true;
file.originalImageURL = originalImageURL;
file.originalVideoURL = originalVideoURL;
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
if (isPlayable) {
file.html = `
<video controls onContextMenu="return false;">
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
`;
} else {
file.html = `
<div class="pswp-item-container">
<img src="${file.msrc}" onContextMenu="return false;"/>
<div class="download-banner" >
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
<a class="btn btn-outline-success" href=${convertedVideoURL} download="${file.metadata.title}"">Download</a>
</div>
</div>
`;
}
} else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
if (isPlayable) {
file.html = `
<div class = 'pswp-item-container'>
<img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/>
<video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
</div>
`;
} else {
file.html = `
<div class="pswp-item-container">
<img src="${file.msrc}" onContextMenu="return false;"/>
<div class="download-banner">
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
<button class = "btn btn-outline-success" id = "download-btn-${file.id}">Download</button>
</div>
</div>
`;
}
} else if (file.metadata.fileType === FILE_TYPE.IMAGE) {
file.src = convertedImageURL;
} else {
logError(
Error(`unknown file type - ${file.metadata.fileType}`),
'Unknown file type'
);
file.src = originalURL;
}
}

View file

@ -1,5 +1,4 @@
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import { isDEVSentryENV } from 'constants/sentry';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import { getSentryUserID } from 'utils/user'; import { getSentryUserID } from 'utils/user';
@ -17,12 +16,9 @@ export const logError = async (
addLogLine( addLogLine(
`error: ${error?.name} ${error?.message} ${ `error: ${error?.name} ${error?.message} ${
error?.stack error?.stack
} msg: ${msg} info: ${JSON.stringify(info)}` } msg: ${msg} ${info ? `info: ${JSON.stringify(info)}` : ''}`
); );
} }
if (isDEVSentryENV()) {
console.log(error, { msg, info });
}
Sentry.captureException(err, { Sentry.captureException(err, {
level: Sentry.Severity.Info, level: Sentry.Severity.Info,
user: { id: await getSentryUserID() }, user: { id: await getSentryUserID() },

View file

@ -140,7 +140,8 @@ const englishConstants = {
CREATE: 'Create', CREATE: 'Create',
DOWNLOAD: 'Download', DOWNLOAD: 'Download',
DOWNLOAD_OPTION: 'Download (D)', DOWNLOAD_OPTION: 'Download (D)',
DOWNLOAD_FAVOURITES: 'Download favourites', DOWNLOAD_FAVORITES: 'Download favorites',
DOWNLOAD_UNCATEGORIZED: 'Download uncategorized',
COPY_OPTION: 'Copy as PNG (Ctrl/Cmd - C)', COPY_OPTION: 'Copy as PNG (Ctrl/Cmd - C)',
TOGGLE_FULLSCREEN: 'Toggle fullscreen (F)', TOGGLE_FULLSCREEN: 'Toggle fullscreen (F)',
ZOOM_IN_OUT: 'Zoom in/out', ZOOM_IN_OUT: 'Zoom in/out',
@ -368,8 +369,15 @@ const englishConstants = {
DELETE_COLLECTION_TITLE: 'Delete album?', DELETE_COLLECTION_TITLE: 'Delete album?',
DELETE_COLLECTION: 'Delete album', DELETE_COLLECTION: 'Delete album',
DELETE_COLLECTION_FAILED: 'Album deletion failed, please try again', DELETE_COLLECTION_FAILED: 'Album deletion failed, please try again',
DELETE_COLLECTION_MESSAGE: DELETE_COLLECTION_MESSAGE: () => (
'Files that are unique to this album will be moved to trash, and this album would be deleted.', <p>
Also delete the photos (and videos) present in this album from
<span style={{ color: '#fff' }}> all </span> other albums they are
part of?
</p>
),
DELETE_PHOTOS: 'Delete photos',
KEEP_PHOTOS: 'Keep photos',
SHARE: 'Share', SHARE: 'Share',
SHARE_COLLECTION: 'Share album', SHARE_COLLECTION: 'Share album',
SHARE_WITH_PEOPLE: 'Share with your loved ones', SHARE_WITH_PEOPLE: 'Share with your loved ones',
@ -441,7 +449,7 @@ const englishConstants = {
), ),
NOT_FILE_OWNER: 'You cannot delete files in a shared album', NOT_FILE_OWNER: 'You cannot delete files in a shared album',
ADD_TO_COLLECTION: 'Add to album', ADD_TO_COLLECTION: 'Add to album',
SELECTED: 'Selected', SELECTED: 'selected',
VIDEO_PLAYBACK_FAILED: 'Video format not supported', VIDEO_PLAYBACK_FAILED: 'Video format not supported',
VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD: VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD:
'This video cannot be played on your browser', 'This video cannot be played on your browser',
@ -587,6 +595,8 @@ const englishConstants = {
THUMBNAIL_GENERATION_FAILED_INFO: THUMBNAIL_GENERATION_FAILED_INFO:
'These files were uploaded, but unfortunately we could not generate the thumbnails for them.', 'These files were uploaded, but unfortunately we could not generate the thumbnails for them.',
UPLOAD_TO_COLLECTION: 'Upload to album', UPLOAD_TO_COLLECTION: 'Upload to album',
UNCATEGORIZED: 'Uncategorized',
MOVE_TO_UNCATEGORIZED: 'Move to uncategorized',
ARCHIVE: 'Archive', ARCHIVE: 'Archive',
ARCHIVE_COLLECTION: 'Archive album', ARCHIVE_COLLECTION: 'Archive album',
ARCHIVE_SECTION_NAME: 'Archive', ARCHIVE_SECTION_NAME: 'Archive',
@ -598,7 +608,9 @@ const englishConstants = {
ADD: 'Add', ADD: 'Add',
SORT: 'Sort', SORT: 'Sort',
REMOVE: 'Remove', REMOVE: 'Remove',
YES_REMOVE: 'Yes, remove',
CONFIRM_REMOVE: 'Confirm removal', CONFIRM_REMOVE: 'Confirm removal',
REMOVE_FROM_COLLECTION: 'Remove from album',
TRASH: 'Trash', TRASH: 'Trash',
MOVE_TO_TRASH: 'Move to trash', MOVE_TO_TRASH: 'Move to trash',
TRASH_FILES_MESSAGE: TRASH_FILES_MESSAGE:
@ -620,11 +632,19 @@ const englishConstants = {
LEAVE_SHARED_ALBUM_FAILED: 'failed to leave the album, please try again', LEAVE_SHARED_ALBUM_FAILED: 'failed to leave the album, please try again',
LEAVE_SHARED_ALBUM_MESSAGE: LEAVE_SHARED_ALBUM_MESSAGE:
'You will leave the album, and it will stop being visible to you.', 'You will leave the album, and it will stop being visible to you.',
CONFIRM_REMOVE_MESSAGE: () => ( CONFIRM_SELF_REMOVE_MESSAGE: () => (
<> <>
<p>Are you sure you want to remove these files from the album?</p>
<p> <p>
All files that are unique to this album will be moved to trash Selected items will be removed from this album. Items which are
only in this album will be moved to Uncategorized.
</p>
</>
),
CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE: () => (
<>
<p>
Some of the items you are removing were added by other people,
and you will lose access to them.
</p> </p>
</> </>
), ),
@ -690,7 +710,7 @@ const englishConstants = {
LINK_EXPIRED_MESSAGE: 'This link has either expired or been disabled!', LINK_EXPIRED_MESSAGE: 'This link has either expired or been disabled!',
MANAGE_LINK: 'Manage link', MANAGE_LINK: 'Manage link',
LINK_TOO_MANY_REQUESTS: 'This album is too popular for us to handle!', LINK_TOO_MANY_REQUESTS: 'This album is too popular for us to handle!',
DISABLE_PUBLIC_SHARING: "'Disable public sharing", DISABLE_PUBLIC_SHARING: 'Disable public sharing',
DISABLE_PUBLIC_SHARING_MESSAGE: DISABLE_PUBLIC_SHARING_MESSAGE:
'Are you sure you want to disable public sharing?', 'Are you sure you want to disable public sharing?',
FILE_DOWNLOAD: 'Allow downloads', FILE_DOWNLOAD: 'Allow downloads',
@ -894,6 +914,7 @@ const englishConstants = {
), ),
ADD_X_PHOTOS: (x: number) => `Add ${x} ${x > 1 ? 'photos' : 'photo'}`, ADD_X_PHOTOS: (x: number) => `Add ${x} ${x > 1 ? 'photos' : 'photo'}`,
CHOSE_THEME: 'Choose theme', CHOSE_THEME: 'Choose theme',
YOURS: 'yours',
}; };
export default englishConstants; export default englishConstants;

45
src/utils/user/family.ts Normal file
View file

@ -0,0 +1,45 @@
import { FamilyData, FamilyMember, User } from 'types/user';
import { logError } from 'utils/sentry';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
export function getLocalFamilyData(): FamilyData {
return getData(LS_KEYS.FAMILY_DATA);
}
// isPartOfFamily return true if the current user is part of some family plan
export function isPartOfFamily(familyData: FamilyData): boolean {
return Boolean(
familyData && familyData.members && familyData.members.length > 0
);
}
// hasNonAdminFamilyMembers return true if the admin user has members in his family
export function hasNonAdminFamilyMembers(familyData: FamilyData): boolean {
return Boolean(isPartOfFamily(familyData) && familyData.members.length > 1);
}
export function isFamilyAdmin(familyData: FamilyData): boolean {
const familyAdmin: FamilyMember = getFamilyPlanAdmin(familyData);
const user: User = getData(LS_KEYS.USER);
return familyAdmin.email === user.email;
}
export function getFamilyPlanAdmin(familyData: FamilyData): FamilyMember {
if (isPartOfFamily(familyData)) {
return familyData.members.find((x) => x.isAdmin);
} else {
logError(
Error(
'verify user is part of family plan before calling this method'
),
'invalid getFamilyPlanAdmin call'
);
}
}
export function getTotalFamilyUsage(familyData: FamilyData): number {
return familyData.members.reduce(
(sum, currentMember) => sum + currentMember.usage,
0
);
}

View file

@ -2896,10 +2896,10 @@ jsx-ast-utils@^3.3.2:
array-includes "^3.1.5" array-includes "^3.1.5"
object.assign "^4.1.3" object.assign "^4.1.3"
jszip@3.7.1: jszip@3.8.0:
version "3.7.1" version "3.8.0"
resolved "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.8.0.tgz#a2ac3c33fe96a76489765168213655850254d51b"
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg== integrity sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==
dependencies: dependencies:
lie "~3.3.0" lie "~3.3.0"
pako "~1.0.2" pako "~1.0.2"