Merge branch 'main' into better-comlink-types
This commit is contained in:
commit
2611289052
38
.github/workflows/cla.yaml
vendored
Normal file
38
.github/workflows/cla.yaml
vendored
Normal 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
98
CLA.md
Normal 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.
|
11
README.md
11
README.md
|
@ -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 would like to motivate us to keep building, you can do so by
|
||||
[starring](https://github.com/ente-io/photos-web/stargazers) this project.
|
||||
And [star this repo](https://github.com/ente-io/photos-web/stargazers)!
|
||||
|
||||
<br/>
|
||||
|
||||
## 🌍 Translate
|
||||
|
||||
Create a copy of
|
||||
[src/utils/strings/englishConstants.tsx](src/utils/strings/englishConstants.tsx)
|
||||
and open a PR.
|
||||
|
||||
<br/>
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"formik": "^2.1.5",
|
||||
"heic-convert": "^1.2.4",
|
||||
"is-electron": "^2.2.0",
|
||||
"jszip": "3.7.1",
|
||||
"jszip": "3.8.0",
|
||||
"libsodium-wrappers": "^0.7.8",
|
||||
"localforage": "^1.9.0",
|
||||
"next": "^13.1.2",
|
||||
|
|
|
@ -10,9 +10,13 @@ interface Iprops {
|
|||
action: CollectionActions,
|
||||
loader?: boolean
|
||||
) => (...args: any[]) => Promise<void>;
|
||||
downloadOptionText?: string;
|
||||
}
|
||||
|
||||
export function FavoritiesCollectionOption({ handleCollectionAction }: Iprops) {
|
||||
export function OnlyDownloadCollectionOption({
|
||||
handleCollectionAction,
|
||||
downloadOptionText = constants.DOWNLOAD,
|
||||
}: Iprops) {
|
||||
return (
|
||||
<OverflowMenuOption
|
||||
startIcon={<FileDownloadOutlinedIcon />}
|
||||
|
@ -20,7 +24,7 @@ export function FavoritiesCollectionOption({ handleCollectionAction }: Iprops) {
|
|||
CollectionActions.CONFIRM_DOWNLOAD,
|
||||
false
|
||||
)}>
|
||||
{constants.DOWNLOAD_COLLECTION}
|
||||
{downloadOptionText}
|
||||
</OverflowMenuOption>
|
||||
);
|
||||
}
|
|
@ -25,6 +25,7 @@ export function QuickOptions({
|
|||
{!(
|
||||
collectionSummaryType === CollectionSummaryType.trash ||
|
||||
collectionSummaryType === CollectionSummaryType.favorites ||
|
||||
collectionSummaryType === CollectionSummaryType.uncategorized ||
|
||||
collectionSummaryType === CollectionSummaryType.incomingShare
|
||||
) && (
|
||||
<Tooltip
|
||||
|
@ -51,7 +52,10 @@ export function QuickOptions({
|
|||
title={
|
||||
collectionSummaryType ===
|
||||
CollectionSummaryType.favorites
|
||||
? constants.DOWNLOAD_FAVOURITES
|
||||
? constants.DOWNLOAD_FAVORITES
|
||||
: collectionSummaryType ===
|
||||
CollectionSummaryType.uncategorized
|
||||
? constants.DOWNLOAD_UNCATEGORIZED
|
||||
: constants.DOWNLOAD_COLLECTION
|
||||
}>
|
||||
<IconButton
|
||||
|
|
|
@ -18,7 +18,7 @@ import OverflowMenu from 'components/OverflowMenu/menu';
|
|||
import { CollectionSummaryType } from 'constants/collection';
|
||||
import { TrashCollectionOption } from './TrashCollectionOption';
|
||||
import { SharedCollectionOption } from './SharedCollectionOption';
|
||||
import { FavoritiesCollectionOption } from './FavoritiesCollectionOption';
|
||||
import { OnlyDownloadCollectionOption } from './OnlyDownloadCollectionOption';
|
||||
import { QuickOptions } from './QuickOptions';
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
import { HorizontalFlex } from 'components/Container';
|
||||
|
@ -39,7 +39,8 @@ export enum CollectionActions {
|
|||
ARCHIVE,
|
||||
UNARCHIVE,
|
||||
CONFIRM_DELETE,
|
||||
DELETE,
|
||||
DELETE_WITH_FILES,
|
||||
DELETE_BUT_KEEP_FILES,
|
||||
SHOW_SHARE_DIALOG,
|
||||
CONFIRM_EMPTY_TRASH,
|
||||
EMPTY_TRASH,
|
||||
|
@ -87,8 +88,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
case CollectionActions.CONFIRM_DELETE:
|
||||
callback = confirmDeleteCollection;
|
||||
break;
|
||||
case CollectionActions.DELETE:
|
||||
callback = deleteCollection;
|
||||
case CollectionActions.DELETE_WITH_FILES:
|
||||
callback = deleteCollectionAlongWithFiles;
|
||||
break;
|
||||
case CollectionActions.DELETE_BUT_KEEP_FILES:
|
||||
callback = deleteCollectionButKeepFiles;
|
||||
break;
|
||||
case CollectionActions.SHOW_SHARE_DIALOG:
|
||||
callback = showCollectionShareModal;
|
||||
|
@ -137,8 +141,13 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
}
|
||||
};
|
||||
|
||||
const deleteCollection = async () => {
|
||||
await CollectionAPI.deleteCollection(activeCollection.id);
|
||||
const deleteCollectionAlongWithFiles = async () => {
|
||||
await CollectionAPI.deleteCollection(activeCollection.id, false);
|
||||
redirectToAll();
|
||||
};
|
||||
|
||||
const deleteCollectionButKeepFiles = async () => {
|
||||
await CollectionAPI.deleteCollection(activeCollection.id, true);
|
||||
redirectToAll();
|
||||
};
|
||||
|
||||
|
@ -177,12 +186,21 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
const confirmDeleteCollection = () => {
|
||||
setDialogMessage({
|
||||
title: constants.DELETE_COLLECTION_TITLE,
|
||||
content: constants.DELETE_COLLECTION_MESSAGE,
|
||||
content: constants.DELETE_COLLECTION_MESSAGE(),
|
||||
proceed: {
|
||||
text: constants.DELETE_COLLECTION,
|
||||
action: handleCollectionAction(CollectionActions.DELETE),
|
||||
text: constants.DELETE_PHOTOS,
|
||||
action: handleCollectionAction(
|
||||
CollectionActions.DELETE_WITH_FILES
|
||||
),
|
||||
variant: 'danger',
|
||||
},
|
||||
secondary: {
|
||||
text: constants.KEEP_PHOTOS,
|
||||
action: handleCollectionAction(
|
||||
CollectionActions.DELETE_BUT_KEEP_FILES
|
||||
),
|
||||
variant: 'primary',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
|
@ -250,8 +268,15 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
/>
|
||||
) : collectionSummaryType ===
|
||||
CollectionSummaryType.favorites ? (
|
||||
<FavoritiesCollectionOption
|
||||
<OnlyDownloadCollectionOption
|
||||
handleCollectionAction={handleCollectionAction}
|
||||
downloadOptionText={constants.DOWNLOAD_FAVORITES}
|
||||
/>
|
||||
) : collectionSummaryType ===
|
||||
CollectionSummaryType.uncategorized ? (
|
||||
<OnlyDownloadCollectionOption
|
||||
handleCollectionAction={handleCollectionAction}
|
||||
downloadOptionText={constants.DOWNLOAD_UNCATEGORIZED}
|
||||
/>
|
||||
) : collectionSummaryType ===
|
||||
CollectionSummaryType.incomingShare ? (
|
||||
|
|
|
@ -15,7 +15,8 @@ export default function EmailShare({ collection }) {
|
|||
|
||||
const collectionShare: SingleInputFormProps['callback'] = async (
|
||||
email,
|
||||
setFieldError
|
||||
setFieldError,
|
||||
resetForm
|
||||
) => {
|
||||
try {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
@ -28,6 +29,7 @@ export default function EmailShare({ collection }) {
|
|||
} else {
|
||||
await shareCollection(collection, email);
|
||||
await galleryContext.syncWithRemote(false, true);
|
||||
resetForm();
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMessage = handleSharingErrors(e);
|
||||
|
|
|
@ -96,6 +96,18 @@ export default function DialogBox({
|
|||
{attributes.proceed.text}
|
||||
</Button>
|
||||
)}
|
||||
{attributes.secondary && (
|
||||
<Button
|
||||
size="large"
|
||||
color={attributes.secondary?.variant}
|
||||
onClick={() => {
|
||||
attributes.secondary.action();
|
||||
onClose();
|
||||
}}
|
||||
disabled={attributes.secondary.disabled}>
|
||||
{attributes.secondary.text}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
</DialogActions>
|
||||
)}
|
||||
|
|
|
@ -3,7 +3,7 @@ import VerticallyCentered, { FlexWrapper } from 'components/Container';
|
|||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import billingService from 'services/billingService';
|
||||
import { getFamilyPlanAdmin } from 'utils/billing';
|
||||
import { getFamilyPlanAdmin } from 'utils/user/family';
|
||||
import { preloadImage } from 'utils/common';
|
||||
import constants from 'utils/strings/constants';
|
||||
import DialogTitleWithCloseButton from './DialogBox/TitleWithCloseButton';
|
||||
|
|
|
@ -31,7 +31,7 @@ export function OverflowMenuOption({
|
|||
<MenuItem
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
width: 220,
|
||||
minWidth: 220,
|
||||
color: (theme) => theme.palette[color].main,
|
||||
padding: 1.5,
|
||||
'& .MuiSvgIcon-root': {
|
||||
|
|
|
@ -4,7 +4,6 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
|
|||
import { EnteFile } from 'types/file';
|
||||
import { styled } from '@mui/material';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import constants from 'utils/strings/constants';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import PhotoViewer from 'components/PhotoViewer';
|
||||
import {
|
||||
|
@ -13,10 +12,9 @@ import {
|
|||
TRASH_SECTION,
|
||||
} from 'constants/collection';
|
||||
import { isSharedFile } from 'utils/file';
|
||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||
import { updateFileMsrcProps, updateFileSrcProps } from 'utils/photoFrame';
|
||||
import { PhotoList } from './PhotoList';
|
||||
import { SelectedState } from 'types/gallery';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { MergedSourceURL, SelectedState } from 'types/gallery';
|
||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { useRouter } from 'next/router';
|
||||
|
@ -27,12 +25,12 @@ import { IsArchived } from 'utils/magicMetadata';
|
|||
import { isSameDayAnyYear, isInsideBox } from 'utils/search';
|
||||
import { Search } from 'types/search';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { useMemo } from 'react';
|
||||
import { Collection } from 'types/collection';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import PhotoSwipe from 'photoswipe';
|
||||
|
||||
const Container = styled('div')`
|
||||
display: block;
|
||||
|
@ -66,19 +64,12 @@ interface Props {
|
|||
deletedFileIds?: Set<number>;
|
||||
setDeletedFileIds?: (value: Set<number>) => void;
|
||||
activeCollection: number;
|
||||
isSharedCollection?: boolean;
|
||||
isIncomingSharedCollection?: boolean;
|
||||
enableDownload?: boolean;
|
||||
isDeduplicating?: boolean;
|
||||
resetSearch?: () => void;
|
||||
}
|
||||
|
||||
type SourceURL = {
|
||||
originalImageURL?: string;
|
||||
originalVideoURL?: string;
|
||||
convertedImageURL?: string;
|
||||
convertedVideoURL?: string;
|
||||
};
|
||||
|
||||
const PhotoFrame = ({
|
||||
files,
|
||||
collections,
|
||||
|
@ -95,10 +86,11 @@ const PhotoFrame = ({
|
|||
deletedFileIds,
|
||||
setDeletedFileIds,
|
||||
activeCollection,
|
||||
isSharedCollection,
|
||||
isIncomingSharedCollection,
|
||||
enableDownload,
|
||||
isDeduplicating,
|
||||
}: Props) => {
|
||||
const [user, setUser] = useState<User>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
|
||||
|
@ -119,6 +111,11 @@ const PhotoFrame = ({
|
|||
|
||||
const [filteredData, setFilteredData] = useState<EnteFile[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
setUser(user);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const main = () => {
|
||||
if (updateInProgress.current) {
|
||||
|
@ -130,13 +127,6 @@ const PhotoFrame = ({
|
|||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
const filteredData = files
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
dataIndex: index,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
title: item.pubMagicMetadata?.data.caption,
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (
|
||||
deletedFileIds?.has(item.id) &&
|
||||
|
@ -179,7 +169,10 @@ const PhotoFrame = ({
|
|||
return false;
|
||||
}
|
||||
|
||||
if (isSharedFile(user, item) && !isSharedCollection) {
|
||||
if (
|
||||
isSharedFile(user, item) &&
|
||||
activeCollection !== item.collectionID
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === TRASH_SECTION && !item.isTrashed) {
|
||||
|
@ -202,6 +195,31 @@ const PhotoFrame = ({
|
|||
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);
|
||||
updateInProgress.current = false;
|
||||
|
@ -221,6 +239,10 @@ const PhotoFrame = ({
|
|||
activeCollection,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
setFetching({});
|
||||
}, [filteredData]);
|
||||
|
||||
const fileToCollectionsMap = useMemo(() => {
|
||||
const fileToCollectionsMap = new Map<number, number[]>();
|
||||
files.forEach((file) => {
|
||||
|
@ -273,6 +295,7 @@ const PhotoFrame = ({
|
|||
};
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
|
||||
router.events.on('hashChangeComplete', (url: string) => {
|
||||
const start = url.indexOf('#');
|
||||
const hash = url.slice(start !== -1 ? start : url.length);
|
||||
|
@ -285,9 +308,10 @@ const PhotoFrame = ({
|
|||
setOpen(false);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
document.removeEventListener('keydown', handleKeyDown, false);
|
||||
document.removeEventListener('keyup', handleKeyUp, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -303,127 +327,61 @@ const PhotoFrame = ({
|
|||
}
|
||||
}, [search, filteredData]);
|
||||
|
||||
const resetFetching = () => {
|
||||
setFetching({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.count === 0) {
|
||||
setRangeStart(null);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const getFileIndexFromID = (files: EnteFile[], id: number) => {
|
||||
const index = files.findIndex((file) => file.id === id);
|
||||
if (index === -1) {
|
||||
throw CustomError.FILE_ID_NOT_FOUND;
|
||||
const updateURL = (index: number) => (id: number, url: string) => {
|
||||
const file = filteredData[index];
|
||||
// this is to prevent outdated updateURL call from updating the wrong file
|
||||
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 updateFile = (file: EnteFile) => {
|
||||
file.msrc = url;
|
||||
file.w = window.innerWidth;
|
||||
file.h = window.innerHeight;
|
||||
|
||||
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
|
||||
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
|
||||
) {
|
||||
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
|
||||
) {
|
||||
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;
|
||||
};
|
||||
const updateSrcURL = async (
|
||||
index: number,
|
||||
id: number,
|
||||
mergedSrcURL: MergedSourceURL
|
||||
) => {
|
||||
const file = filteredData[index];
|
||||
// this is to prevent outdate updateSrcURL call from updating the wrong file
|
||||
if (file.id !== id) {
|
||||
addLogLine(
|
||||
`PhotoSwipe: updateSrcURL: file id mismatch: ${file.id} !== ${id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (file.isSourceLoaded) {
|
||||
addLogLine(
|
||||
`PhotoSwipe: updateSrcURL: source already loaded: ${file.id}`
|
||||
);
|
||||
logError(
|
||||
new Error(
|
||||
`PhotoSwipe: updateSrcURL: source already loaded: ${file.id}`
|
||||
),
|
||||
'PhotoSwipe updateSrcURL called when source already loaded'
|
||||
);
|
||||
return;
|
||||
}
|
||||
await updateFileSrcProps(file, mergedSrcURL);
|
||||
setIsSourceLoaded(true);
|
||||
const index = getFileIndexFromID(files, id);
|
||||
return updateFile(files[index]);
|
||||
};
|
||||
|
||||
const handleClose = (needUpdate) => {
|
||||
|
@ -436,30 +394,52 @@ const PhotoFrame = ({
|
|||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleSelect = (id: number, index?: number) => (checked: boolean) => {
|
||||
if (selected.collectionID !== activeCollection) {
|
||||
setSelected({ count: 0, collectionID: 0 });
|
||||
}
|
||||
if (typeof index !== 'undefined') {
|
||||
if (checked) {
|
||||
setRangeStart(index);
|
||||
} else {
|
||||
setRangeStart(undefined);
|
||||
const handleSelect =
|
||||
(id: number, isOwnFile: boolean, index?: number) =>
|
||||
(checked: boolean) => {
|
||||
if (typeof index !== 'undefined') {
|
||||
if (checked) {
|
||||
setRangeStart(index);
|
||||
} else {
|
||||
setRangeStart(undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
setSelected((selected) => {
|
||||
if (selected.collectionID !== activeCollection) {
|
||||
selected = { ownCount: 0, count: 0, collectionID: 0 };
|
||||
}
|
||||
|
||||
setSelected((selected) => ({
|
||||
...selected,
|
||||
[id]: checked,
|
||||
count:
|
||||
selected[id] === checked
|
||||
? selected.count
|
||||
: checked
|
||||
? selected.count + 1
|
||||
: selected.count - 1,
|
||||
collectionID: activeCollection,
|
||||
}));
|
||||
};
|
||||
const handleCounterChange = (count: number) => {
|
||||
if (selected[id] === checked) {
|
||||
return count;
|
||||
}
|
||||
if (checked) {
|
||||
return count + 1;
|
||||
} else {
|
||||
return count - 1;
|
||||
}
|
||||
};
|
||||
|
||||
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) => () => {
|
||||
setCurrentHover(index);
|
||||
};
|
||||
|
@ -481,56 +461,59 @@ const PhotoFrame = ({
|
|||
(index - i) * direction > 0;
|
||||
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 = (
|
||||
files: EnteFile[],
|
||||
item: EnteFile,
|
||||
index: number,
|
||||
isScrolling: boolean
|
||||
) =>
|
||||
files[index] ? (
|
||||
<PreviewCard
|
||||
key={`tile-${files[index].id}-selected-${
|
||||
selected[files[index].id] ?? false
|
||||
}`}
|
||||
file={files[index]}
|
||||
updateURL={updateURL(files[index].id)}
|
||||
onClick={onThumbnailClick(index)}
|
||||
selectable={!isSharedCollection}
|
||||
onSelect={handleSelect(files[index].id, index)}
|
||||
selected={
|
||||
selected.collectionID === activeCollection &&
|
||||
selected[files[index].id]
|
||||
}
|
||||
selectOnClick={selected.count > 0}
|
||||
onHover={onHoverOver(index)}
|
||||
onRangeSelect={handleRangeSelect(index)}
|
||||
isRangeSelectActive={isShiftKeyPressed && selected.count > 0}
|
||||
isInsSelectRange={
|
||||
(index >= rangeStart && index <= currentHover) ||
|
||||
(index >= currentHover && index <= rangeStart)
|
||||
}
|
||||
activeCollection={activeCollection}
|
||||
showPlaceholder={isScrolling}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
) => (
|
||||
<PreviewCard
|
||||
key={`tile-${item.id}-selected-${selected[item.id] ?? false}`}
|
||||
file={item}
|
||||
updateURL={updateURL(index)}
|
||||
onClick={onThumbnailClick(index)}
|
||||
selectable={
|
||||
!publicCollectionGalleryContext?.accessedThroughSharedURL
|
||||
}
|
||||
onSelect={handleSelect(item.id, item.ownerID === user?.id, index)}
|
||||
selected={
|
||||
selected.collectionID === activeCollection && selected[item.id]
|
||||
}
|
||||
selectOnClick={selected.count > 0}
|
||||
onHover={onHoverOver(index)}
|
||||
onRangeSelect={handleRangeSelect(index)}
|
||||
isRangeSelectActive={isShiftKeyPressed && selected.count > 0}
|
||||
isInsSelectRange={
|
||||
(index >= rangeStart && index <= currentHover) ||
|
||||
(index >= currentHover && index <= rangeStart)
|
||||
}
|
||||
activeCollection={activeCollection}
|
||||
showPlaceholder={isScrolling}
|
||||
/>
|
||||
);
|
||||
|
||||
const getSlideData = async (
|
||||
instance: any,
|
||||
instance: PhotoSwipe<PhotoSwipe.Options>,
|
||||
index: number,
|
||||
item: EnteFile
|
||||
) => {
|
||||
addLogLine(
|
||||
`[${
|
||||
item.id
|
||||
}] getSlideData called for thumbnail:${!!item.msrc} original:${
|
||||
!!item.msrc && item.src !== item.msrc
|
||||
} inProgress:${fetching[item.id]}`
|
||||
}] getSlideData called for thumbnail:${!!item.msrc} sourceLoaded:${isSourceLoaded} fetching:${
|
||||
fetching[item.id]
|
||||
}`
|
||||
);
|
||||
if (!item.msrc) {
|
||||
addLogLine(`[${item.id}] doesn't have thumbnail`);
|
||||
|
@ -559,22 +542,15 @@ const PhotoFrame = ({
|
|||
}
|
||||
galleryContext.thumbs.set(item.id, url);
|
||||
}
|
||||
const newFile = updateURL(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`
|
||||
);
|
||||
updateURL(index)(item.id, url);
|
||||
try {
|
||||
addLogLine(
|
||||
`[${
|
||||
item.id
|
||||
}] calling invalidateCurrItems for thumbnail msrc :${!!item.msrc}`
|
||||
);
|
||||
instance.invalidateCurrItems();
|
||||
if (instance.isOpen()) {
|
||||
if ((instance as any).isOpen()) {
|
||||
instance.updateSize(true);
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -588,100 +564,76 @@ const PhotoFrame = ({
|
|||
logError(e, 'getSlideData failed get msrc url failed');
|
||||
}
|
||||
}
|
||||
if (!fetching[item.id]) {
|
||||
addLogLine(`[${item.id}] new file download fetch original request`);
|
||||
try {
|
||||
fetching[item.id] = true;
|
||||
let urls: { original: string[]; converted: string[] };
|
||||
if (galleryContext.files.has(item.id)) {
|
||||
addLogLine(
|
||||
`[${item.id}] gallery context cache hit, using cached file`
|
||||
);
|
||||
const mergedURL = galleryContext.files.get(item.id);
|
||||
urls = {
|
||||
original: mergedURL.original.split(','),
|
||||
converted: mergedURL.converted.split(','),
|
||||
};
|
||||
} else {
|
||||
addLogLine(
|
||||
`[${item.id}] gallery context cache miss, calling downloadManager to get file`
|
||||
);
|
||||
appContext.startLoading();
|
||||
if (
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
) {
|
||||
urls = await PublicCollectionDownloadManager.getFile(
|
||||
if (item.isSourceLoaded) {
|
||||
addLogLine(`[${item.id}] source already loaded`);
|
||||
return;
|
||||
}
|
||||
if (fetching[item.id]) {
|
||||
addLogLine(`[${item.id}] file download already in progress`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
addLogLine(`[${item.id}] new file src request`);
|
||||
fetching[item.id] = true;
|
||||
let srcURL: MergedSourceURL;
|
||||
if (galleryContext.files.has(item.id)) {
|
||||
addLogLine(
|
||||
`[${item.id}] gallery context cache hit, using cached file`
|
||||
);
|
||||
srcURL = galleryContext.files.get(item.id);
|
||||
} else {
|
||||
addLogLine(
|
||||
`[${item.id}] gallery context cache miss, calling downloadManager to get file`
|
||||
);
|
||||
appContext.startLoading();
|
||||
let downloadedURL;
|
||||
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
|
||||
downloadedURL =
|
||||
await PublicCollectionDownloadManager.getFile(
|
||||
item,
|
||||
publicCollectionGalleryContext.token,
|
||||
publicCollectionGalleryContext.passwordToken,
|
||||
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 {
|
||||
[originalImageURL] = urls.original;
|
||||
[convertedImageURL] = urls.converted;
|
||||
downloadedURL = await DownloadManager.getFile(item, true);
|
||||
}
|
||||
setIsSourceLoaded(false);
|
||||
const newFile = await updateSrcURL(item.id, {
|
||||
originalImageURL,
|
||||
originalVideoURL,
|
||||
convertedImageURL,
|
||||
convertedVideoURL,
|
||||
});
|
||||
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;
|
||||
try {
|
||||
addLogLine(
|
||||
`[${item.id}] calling invalidateCurrItems for src`
|
||||
);
|
||||
instance.invalidateCurrItems();
|
||||
if (instance.isOpen()) {
|
||||
instance.updateSize(true);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(
|
||||
e,
|
||||
'updating photoswipe after src url update failed'
|
||||
);
|
||||
throw e;
|
||||
appContext.finishLoading();
|
||||
const mergedURL: MergedSourceURL = {
|
||||
original: downloadedURL.original.join(','),
|
||||
converted: downloadedURL.converted.join(','),
|
||||
};
|
||||
galleryContext.files.set(item.id, mergedURL);
|
||||
srcURL = mergedURL;
|
||||
}
|
||||
setIsSourceLoaded(false);
|
||||
await updateSrcURL(index, item.id, srcURL);
|
||||
|
||||
try {
|
||||
addLogLine(
|
||||
`[${item.id}] calling invalidateCurrItems for src, source loaded :${item.isSourceLoaded}`
|
||||
);
|
||||
instance.invalidateCurrItems();
|
||||
if ((instance as any).isOpen()) {
|
||||
instance.updateSize(true);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'getSlideData failed get src url failed');
|
||||
fetching[item.id] = false;
|
||||
// no-op
|
||||
logError(e, 'updating photoswipe after src url update failed');
|
||||
throw e;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'getSlideData failed get src url failed');
|
||||
fetching[item.id] = false;
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isFirstLoad && files.length === 0 && !isInSearchMode ? (
|
||||
{!isFirstLoad &&
|
||||
files.length === 0 &&
|
||||
!isInSearchMode &&
|
||||
activeCollection === ALL_SECTION ? (
|
||||
<EmptyScreen openUploader={openUploader} />
|
||||
) : (
|
||||
<Container>
|
||||
|
@ -698,7 +650,6 @@ const PhotoFrame = ({
|
|||
!isInSearchMode &&
|
||||
!deduplicateContext.isOnDeduplicatePage
|
||||
}
|
||||
resetFetching={resetFetching}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
|
@ -711,7 +662,7 @@ const PhotoFrame = ({
|
|||
favItemIds={favItemIds}
|
||||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
isSharedCollection={isSharedCollection}
|
||||
isIncomingSharedCollection={isIncomingSharedCollection}
|
||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||
enableDownload={enableDownload}
|
||||
isSourceLoaded={isSourceLoaded}
|
||||
|
|
|
@ -163,12 +163,11 @@ interface Props {
|
|||
filteredData: EnteFile[];
|
||||
showAppDownloadBanner: boolean;
|
||||
getThumbnail: (
|
||||
files: EnteFile[],
|
||||
file: EnteFile,
|
||||
index: number,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element;
|
||||
activeCollection: number;
|
||||
resetFetching: () => void;
|
||||
}
|
||||
|
||||
export function PhotoList({
|
||||
|
@ -178,7 +177,6 @@ export function PhotoList({
|
|||
showAppDownloadBanner,
|
||||
getThumbnail,
|
||||
activeCollection,
|
||||
resetFetching,
|
||||
}: Props) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
|
@ -205,7 +203,6 @@ export function PhotoList({
|
|||
|
||||
const refreshList = () => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
resetFetching();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -657,7 +654,7 @@ export function PhotoList({
|
|||
case ITEM_TYPE.FILE: {
|
||||
const ret = listItem.items.map((item, idx) =>
|
||||
getThumbnail(
|
||||
filteredData,
|
||||
item,
|
||||
listItem.itemStartIndex + idx,
|
||||
isScrolling
|
||||
)
|
||||
|
@ -666,7 +663,11 @@ export function PhotoList({
|
|||
let sum = 0;
|
||||
for (let i = 0; i < listItem.groups.length - 1; 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;
|
||||
}
|
||||
}
|
||||
|
@ -690,7 +691,7 @@ export function PhotoList({
|
|||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
itemKey={generateKey}
|
||||
overscanCount={0}
|
||||
overscanCount={3}
|
||||
useIsScrolling>
|
||||
{({ index, style, isScrolling }) => (
|
||||
<ListItem style={style}>
|
||||
|
|
|
@ -68,7 +68,7 @@ interface Iprops {
|
|||
favItemIds: Set<number>;
|
||||
deletedFileIds: Set<number>;
|
||||
setDeletedFileIds?: (value: Set<number>) => void;
|
||||
isSharedCollection: boolean;
|
||||
isIncomingSharedCollection: boolean;
|
||||
isTrashCollection: boolean;
|
||||
enableDownload: boolean;
|
||||
isSourceLoaded: boolean;
|
||||
|
@ -163,10 +163,6 @@ function PhotoViewer(props: Iprops) {
|
|||
};
|
||||
}, [isOpen, photoSwipe, showInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
updateItems(items);
|
||||
}, [items]);
|
||||
|
||||
useEffect(() => {
|
||||
if (photoSwipe) {
|
||||
photoSwipe.options.arrowKeys = !showInfo;
|
||||
|
@ -386,42 +382,64 @@ function PhotoViewer(props: Iprops) {
|
|||
|
||||
const trashFile = async (file: EnteFile) => {
|
||||
const { deletedFileIds, setDeletedFileIds } = props;
|
||||
deletedFileIds.add(file.id);
|
||||
setDeletedFileIds(new Set(deletedFileIds));
|
||||
await trashFiles([file]);
|
||||
needUpdate.current = true;
|
||||
try {
|
||||
appContext.startLoading();
|
||||
await trashFiles([file]);
|
||||
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) => {
|
||||
if (!file || props.isSharedCollection || props.isTrashCollection) {
|
||||
if (
|
||||
!file ||
|
||||
props.isIncomingSharedCollection ||
|
||||
props.isTrashCollection
|
||||
) {
|
||||
return;
|
||||
}
|
||||
appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
|
||||
};
|
||||
|
||||
const updateItems = (items = []) => {
|
||||
if (photoSwipe) {
|
||||
if (items.length === 0) {
|
||||
photoSwipe.close();
|
||||
}
|
||||
photoSwipe.items.length = 0;
|
||||
items.forEach((item) => {
|
||||
photoSwipe.items.push(item);
|
||||
});
|
||||
photoSwipe.invalidateCurrItems();
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) {
|
||||
photoSwipe.goTo(0);
|
||||
const updateItems = (items: EnteFile[]) => {
|
||||
try {
|
||||
if (photoSwipe) {
|
||||
if (items.length === 0) {
|
||||
photoSwipe.close();
|
||||
}
|
||||
photoSwipe.items.length = 0;
|
||||
items.forEach((item) => {
|
||||
photoSwipe.items.push(item);
|
||||
});
|
||||
|
||||
photoSwipe.invalidateCurrItems();
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
if (
|
||||
photoSwipe.getCurrentIndex() >= photoSwipe.items.length
|
||||
) {
|
||||
photoSwipe.goTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'updateItems failed');
|
||||
}
|
||||
};
|
||||
|
||||
const refreshPhotoswipe = () => {
|
||||
photoSwipe.invalidateCurrItems();
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
try {
|
||||
photoSwipe.invalidateCurrItems();
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'refreshPhotoswipe failed');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -560,7 +578,7 @@ function PhotoViewer(props: Iprops) {
|
|||
<ContentCopy fontSize="small" />
|
||||
</button>
|
||||
)}
|
||||
{!props.isSharedCollection &&
|
||||
{!props.isIncomingSharedCollection &&
|
||||
!props.isTrashCollection && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
|
@ -582,7 +600,7 @@ function PhotoViewer(props: Iprops) {
|
|||
title={constants.TOGGLE_FULLSCREEN}
|
||||
/>
|
||||
|
||||
{!props.isSharedCollection && (
|
||||
{!props.isIncomingSharedCollection && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
title={constants.INFO_OPTION}
|
||||
|
@ -590,7 +608,7 @@ function PhotoViewer(props: Iprops) {
|
|||
<InfoIcon fontSize="small" />
|
||||
</button>
|
||||
)}
|
||||
{!props.isSharedCollection &&
|
||||
{!props.isIncomingSharedCollection &&
|
||||
!props.isTrashCollection && (
|
||||
<button
|
||||
title={
|
||||
|
@ -641,7 +659,7 @@ function PhotoViewer(props: Iprops) {
|
|||
</div>
|
||||
<FileInfo
|
||||
isTrashCollection={props.isTrashCollection}
|
||||
shouldDisableEdits={props.isSharedCollection}
|
||||
shouldDisableEdits={props.isIncomingSharedCollection}
|
||||
showInfo={showInfo}
|
||||
handleCloseInfo={handleCloseInfo}
|
||||
file={photoSwipe?.currItem as EnteFile}
|
||||
|
|
|
@ -20,12 +20,11 @@ const ShortcutButton: FC<ButtonProps<'button', Iprops>> = ({
|
|||
sx={{ fontWeight: 'normal' }}
|
||||
{...props}>
|
||||
{label}
|
||||
{count > 0 && (
|
||||
<Box sx={{ color: 'text.secondary' }}>
|
||||
<DotSeparator />
|
||||
{count}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ color: 'text.secondary' }}>
|
||||
<DotSeparator />
|
||||
{count}
|
||||
</Box>
|
||||
</SidebarButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import React, { useContext } from 'react';
|
||||
import React, { useContext, useState, useEffect } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
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 ShortcutButton from './ShortcutButton';
|
||||
import DeleteOutline from '@mui/icons-material/DeleteOutline';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
import CategoryIcon from '@mui/icons-material/Category';
|
||||
import { getUncategorizedCollection } from 'services/collectionService';
|
||||
interface Iprops {
|
||||
closeSidebar: () => void;
|
||||
collectionSummaries: CollectionSummaries;
|
||||
|
@ -16,6 +22,24 @@ export default function ShortcutSection({
|
|||
collectionSummaries,
|
||||
}: Iprops) {
|
||||
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 = () => {
|
||||
galleryContext.setActiveCollection(TRASH_SECTION);
|
||||
|
@ -26,9 +50,17 @@ export default function ShortcutSection({
|
|||
galleryContext.setActiveCollection(ARCHIVE_SECTION);
|
||||
closeSidebar();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ShortcutButton
|
||||
startIcon={<CategoryIcon />}
|
||||
label={constants.UNCATEGORIZED}
|
||||
onClick={openUncategorizedSection}
|
||||
count={
|
||||
collectionSummaries.get(uncategorizedCollectionId)
|
||||
?.fileCount
|
||||
}
|
||||
/>
|
||||
<ShortcutButton
|
||||
startIcon={<DeleteOutline />}
|
||||
label={constants.TRASH}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { UserDetails } from 'types/user';
|
||||
import { isPartOfFamily } from 'utils/billing';
|
||||
import { isPartOfFamily } from 'utils/user/family';
|
||||
import StorageSection from '../storageSection';
|
||||
import { FamilyUsageSection } from './usageSection';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IndividualSubscriptionCardContent } from './individual';
|
||||
import { FamilySubscriptionCardContent } from './family';
|
||||
import React from 'react';
|
||||
import { hasNonAdminFamilyMembers } from 'utils/billing';
|
||||
import { hasNonAdminFamilyMembers } from 'utils/user/family';
|
||||
import { Overlay, SpaceBetweenFlex } from 'components/Container';
|
||||
import { UserDetails } from 'types/user';
|
||||
|
||||
|
|
|
@ -2,9 +2,7 @@ import { GalleryContext } from 'pages/gallery';
|
|||
import React, { MouseEventHandler, useContext, useMemo } from 'react';
|
||||
import {
|
||||
hasPaidSubscription,
|
||||
isFamilyAdmin,
|
||||
isOnFreePlan,
|
||||
isPartOfFamily,
|
||||
hasExceededStorageQuota,
|
||||
isSubscriptionActive,
|
||||
isSubscriptionCancelled,
|
||||
|
@ -15,6 +13,7 @@ import { UserDetails } from 'types/user';
|
|||
import constants from 'utils/strings/constants';
|
||||
import { Typography } from '@mui/material';
|
||||
import billingService from 'services/billingService';
|
||||
import { isPartOfFamily, isFamilyAdmin } from 'utils/user/family';
|
||||
|
||||
export default function SubscriptionStatus({
|
||||
userDetails,
|
||||
|
|
|
@ -9,7 +9,7 @@ import SubscriptionStatus from './SubscriptionStatus';
|
|||
import { Box, Skeleton } from '@mui/material';
|
||||
import { MemberSubscriptionManage } from '../MemberSubscriptionManage';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { isPartOfFamily, isFamilyAdmin } from 'utils/billing';
|
||||
import { isFamilyAdmin, isPartOfFamily } from 'utils/user/family';
|
||||
|
||||
export default function UserDetailsSection({ sidebarView }) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Formik, FormikHelpers } from 'formik';
|
||||
import { Formik, FormikHelpers, FormikState } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import SubmitButton from './SubmitButton';
|
||||
import TextField from '@mui/material/TextField';
|
||||
|
@ -14,7 +14,8 @@ interface formValues {
|
|||
export interface SingleInputFormProps {
|
||||
callback: (
|
||||
inputValue: string,
|
||||
setFieldError: (errorMessage: string) => void
|
||||
setFieldError: (errorMessage: string) => void,
|
||||
resetForm: (nextState?: Partial<FormikState<formValues>>) => void
|
||||
) => Promise<void>;
|
||||
fieldType: 'text' | 'email' | 'password';
|
||||
placeholder: string;
|
||||
|
@ -43,10 +44,11 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
|||
{ setFieldError, resetForm }: FormikHelpers<formValues>
|
||||
) => {
|
||||
SetLoading(true);
|
||||
await props.callback(values.inputValue, (message) =>
|
||||
setFieldError('inputValue', message)
|
||||
await props.callback(
|
||||
values.inputValue,
|
||||
(message) => setFieldError('inputValue', message),
|
||||
resetForm
|
||||
);
|
||||
resetForm();
|
||||
SetLoading(false);
|
||||
};
|
||||
|
||||
|
|
|
@ -533,7 +533,7 @@ export default function Uploader(props: Props) {
|
|||
}
|
||||
};
|
||||
|
||||
function showUserFacingError(err: CustomError) {
|
||||
function showUserFacingError(err: string) {
|
||||
let notification: NotificationAttributes;
|
||||
switch (err) {
|
||||
case CustomError.SESSION_EXPIRED:
|
||||
|
|
|
@ -11,8 +11,6 @@ import {
|
|||
hasMobileSubscription,
|
||||
getLocalUserSubscription,
|
||||
hasPaidSubscription,
|
||||
getTotalFamilyUsage,
|
||||
isPartOfFamily,
|
||||
isSubscriptionActive,
|
||||
} from 'utils/billing';
|
||||
import { reverseString } from 'utils/common';
|
||||
|
@ -28,6 +26,7 @@ import { getLocalUserDetails } from 'utils/user';
|
|||
import { PLAN_PERIOD } from 'constants/gallery';
|
||||
import FreeSubscriptionPlanSelectorCard from './free';
|
||||
import PaidSubscriptionPlanSelectorCard from './paid';
|
||||
import { isPartOfFamily, getTotalFamilyUsage } from 'utils/user/family';
|
||||
|
||||
interface Props {
|
||||
closeModal: any;
|
||||
|
|
|
@ -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 { styled } from '@mui/material';
|
||||
import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined';
|
||||
|
@ -18,7 +18,7 @@ import { formatDateRelative } from 'utils/time/format';
|
|||
|
||||
interface IProps {
|
||||
file: EnteFile;
|
||||
updateURL: (url: string) => EnteFile;
|
||||
updateURL: (id: number, url: string) => void;
|
||||
onClick: () => void;
|
||||
selectable: boolean;
|
||||
selected: boolean;
|
||||
|
@ -197,8 +197,8 @@ const Cont = styled('div')<{ disabled: boolean }>`
|
|||
`;
|
||||
|
||||
export default function PreviewCard(props: IProps) {
|
||||
const [imgSrc, setImgSrc] = useState<string>();
|
||||
const { thumbs } = useContext(GalleryContext);
|
||||
|
||||
const {
|
||||
file,
|
||||
onClick,
|
||||
|
@ -212,56 +212,57 @@ export default function PreviewCard(props: IProps) {
|
|||
isRangeSelectActive,
|
||||
isInsSelectRange,
|
||||
} = props;
|
||||
|
||||
const [imgSrc, setImgSrc] = useState<string>(file.msrc);
|
||||
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
const deduplicateContext = useContext(DeduplicateContext);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (file && !file.msrc && !props.showPlaceholder) {
|
||||
const isMounted = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!file.msrc && !props.showPlaceholder) {
|
||||
const main = async () => {
|
||||
try {
|
||||
let url;
|
||||
if (
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
) {
|
||||
url =
|
||||
await PublicCollectionDownloadManager.getThumbnail(
|
||||
file,
|
||||
publicCollectionGalleryContext.token,
|
||||
publicCollectionGalleryContext.passwordToken
|
||||
);
|
||||
if (thumbs.has(file.id)) {
|
||||
url = thumbs.get(file.id);
|
||||
} 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);
|
||||
thumbs.set(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;
|
||||
updateURL(file.id, url);
|
||||
} catch (e) {
|
||||
logError(e, 'preview card useEffect failed');
|
||||
// no-op
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
main();
|
||||
}
|
||||
}, [file, props.showPlaceholder]);
|
||||
}, [props.showPlaceholder]);
|
||||
|
||||
const handleClick = () => {
|
||||
if (selectOnClick) {
|
||||
|
@ -294,7 +295,7 @@ export default function PreviewCard(props: IProps) {
|
|||
|
||||
return (
|
||||
<Cont
|
||||
id={`thumb-${file?.id}-${props.showPlaceholder}`}
|
||||
key={`thumb-${file.id}-${props.showPlaceholder}`}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleHover}
|
||||
disabled={!file?.msrc && !imgSrc}
|
||||
|
|
|
@ -35,11 +35,14 @@ interface Props {
|
|||
fixTimeHelper: () => void;
|
||||
downloadHelper: () => void;
|
||||
count: number;
|
||||
ownCount: number;
|
||||
clearSelection: () => void;
|
||||
archiveFilesHelper: () => void;
|
||||
unArchiveFilesHelper: () => void;
|
||||
activeCollection: number;
|
||||
isFavoriteCollection: boolean;
|
||||
isUncategorizedCollection: boolean;
|
||||
isIncomingSharedCollection: boolean;
|
||||
}
|
||||
|
||||
const SelectedFileOptions = ({
|
||||
|
@ -53,11 +56,14 @@ const SelectedFileOptions = ({
|
|||
deleteFileHelper,
|
||||
downloadHelper,
|
||||
count,
|
||||
ownCount,
|
||||
clearSelection,
|
||||
archiveFilesHelper,
|
||||
unArchiveFilesHelper,
|
||||
activeCollection,
|
||||
isFavoriteCollection,
|
||||
isUncategorizedCollection,
|
||||
isIncomingSharedCollection,
|
||||
}: Props) => {
|
||||
const { setDialogMessage } = useContext(AppContext);
|
||||
const addToCollection = () =>
|
||||
|
@ -92,18 +98,33 @@ const SelectedFileOptions = ({
|
|||
title: constants.RESTORE_TO_COLLECTION,
|
||||
});
|
||||
|
||||
const removeFromCollectionHandler = () =>
|
||||
setDialogMessage({
|
||||
title: constants.CONFIRM_REMOVE,
|
||||
content: constants.CONFIRM_REMOVE_MESSAGE(),
|
||||
const removeFromCollectionHandler = () => {
|
||||
if (ownCount === count) {
|
||||
setDialogMessage({
|
||||
title: constants.REMOVE_FROM_COLLECTION,
|
||||
content: constants.CONFIRM_SELF_REMOVE_MESSAGE(),
|
||||
|
||||
proceed: {
|
||||
action: removeFromCollectionHelper,
|
||||
text: constants.REMOVE,
|
||||
variant: 'danger',
|
||||
},
|
||||
close: { text: constants.CANCEL },
|
||||
});
|
||||
proceed: {
|
||||
action: removeFromCollectionHelper,
|
||||
text: constants.YES_REMOVE,
|
||||
variant: 'primary',
|
||||
},
|
||||
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 = () => {
|
||||
setCollectionSelectorAttributes({
|
||||
|
@ -121,7 +142,8 @@ const SelectedFileOptions = ({
|
|||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Box ml={1.5}>
|
||||
{count} {constants.SELECTED}
|
||||
{count} {constants.SELECTED}{' '}
|
||||
{ownCount !== count && `(${ownCount} ${constants.YOURS})`}
|
||||
</Box>
|
||||
</FluidContainer>
|
||||
<Stack spacing={2} direction="row" mr={2}>
|
||||
|
@ -138,6 +160,30 @@ const SelectedFileOptions = ({
|
|||
</IconButton>
|
||||
</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}>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
export const ARCHIVE_SECTION = -1;
|
||||
export const TRASH_SECTION = -2;
|
||||
export const DUMMY_UNCATEGORIZED_SECTION = -3;
|
||||
export const ALL_SECTION = 0;
|
||||
|
||||
export enum CollectionType {
|
||||
folder = 'folder',
|
||||
favorites = 'favorites',
|
||||
album = 'album',
|
||||
uncategorized = 'uncategorized',
|
||||
}
|
||||
|
||||
export enum CollectionSummaryType {
|
||||
|
@ -14,6 +15,7 @@ export enum CollectionSummaryType {
|
|||
album = 'album',
|
||||
archive = 'archive',
|
||||
trash = 'trash',
|
||||
uncategorized = 'uncategorized',
|
||||
all = 'all',
|
||||
outgoingShare = 'outgoingShare',
|
||||
incomingShare = 'incomingShare',
|
||||
|
@ -26,6 +28,9 @@ export enum COLLECTION_SORT_BY {
|
|||
UPDATION_TIME_DESCENDING,
|
||||
}
|
||||
|
||||
export const UNCATEGORIZED_COLLECTION_NAME = 'Uncategorized';
|
||||
export const FAVORITE_COLLECTION_NAME = 'Favorites';
|
||||
|
||||
export const COLLECTION_SHARE_DEFAULT_VALID_DURATION =
|
||||
10 * 24 * 60 * 60 * 1000 * 1000;
|
||||
export const COLLECTION_SHARE_DEFAULT_DEVICE_LIMIT = 4;
|
||||
|
@ -41,21 +46,22 @@ export const COLLECTION_SORT_ORDER = new Map([
|
|||
[CollectionSummaryType.archived, 2],
|
||||
[CollectionSummaryType.archive, 3],
|
||||
[CollectionSummaryType.trash, 4],
|
||||
[CollectionSummaryType.uncategorized, 4],
|
||||
]);
|
||||
|
||||
export const SYSTEM_COLLECTION_TYPES = new Set([
|
||||
CollectionSummaryType.all,
|
||||
CollectionSummaryType.archive,
|
||||
CollectionSummaryType.trash,
|
||||
CollectionSummaryType.uncategorized,
|
||||
]);
|
||||
|
||||
export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([
|
||||
CollectionSummaryType.all,
|
||||
CollectionSummaryType.archive,
|
||||
CollectionSummaryType.incomingShare,
|
||||
CollectionSummaryType.outgoingShare,
|
||||
CollectionSummaryType.sharedOnlyViaLink,
|
||||
CollectionSummaryType.trash,
|
||||
CollectionSummaryType.uncategorized,
|
||||
]);
|
||||
|
||||
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([
|
||||
CollectionSummaryType.trash,
|
||||
CollectionSummaryType.archive,
|
||||
CollectionSummaryType.uncategorized,
|
||||
]);
|
||||
|
|
|
@ -15,6 +15,8 @@ export const FILE_TYPE_LIB_MISSED_FORMATS = [
|
|||
{ fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' },
|
||||
];
|
||||
|
||||
export const KNOWN_NON_MEDIA_FORMATS = ['xmp'];
|
||||
|
||||
export const EXIFLESS_FORMATS = ['image/gif'];
|
||||
|
||||
export const EXIF_LIBRARY_UNSUPPORTED_FORMATS = ['image/webp'];
|
||||
|
|
|
@ -257,8 +257,10 @@ export default function App({ Component, err }) {
|
|||
isLoadingBarRunning.current = true;
|
||||
};
|
||||
const finishLoading = () => {
|
||||
isLoadingBarRunning.current && loadingBar.current?.complete();
|
||||
isLoadingBarRunning.current = false;
|
||||
setTimeout(() => {
|
||||
isLoadingBarRunning.current && loadingBar.current?.complete();
|
||||
isLoadingBarRunning.current = false;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const closeMessageDialog = () => setMessageDialogView(false);
|
||||
|
|
|
@ -54,6 +54,7 @@ export default function Deduplicate() {
|
|||
const [selected, setSelected] = useState<SelectedState>({
|
||||
count: 0,
|
||||
collectionID: 0,
|
||||
ownCount: 0,
|
||||
});
|
||||
const closeDeduplication = function () {
|
||||
Router.push(PAGES.GALLERY);
|
||||
|
@ -107,6 +108,7 @@ export default function Deduplicate() {
|
|||
setFileSizeMap(currFileSizeMap);
|
||||
const selectedFiles = {
|
||||
count: count,
|
||||
ownCount: count,
|
||||
collectionID: ALL_SECTION,
|
||||
};
|
||||
for (const fileID of toSelectFileIDs) {
|
||||
|
@ -144,7 +146,7 @@ export default function Deduplicate() {
|
|||
};
|
||||
|
||||
const clearSelection = function () {
|
||||
setSelected({ count: 0, collectionID: 0 });
|
||||
setSelected({ count: 0, collectionID: 0, ownCount: 0 });
|
||||
};
|
||||
|
||||
if (!duplicateFiles) {
|
||||
|
|
|
@ -60,18 +60,19 @@ import Uploader from 'components/Upload/Uploader';
|
|||
import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
CollectionSummaryType,
|
||||
CollectionType,
|
||||
DUMMY_UNCATEGORIZED_SECTION,
|
||||
TRASH_SECTION,
|
||||
UNCATEGORIZED_COLLECTION_NAME,
|
||||
} from 'constants/collection';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { CustomError, ServerErrorCodes } from 'utils/error';
|
||||
import { PAGES } from 'constants/pages';
|
||||
import {
|
||||
COLLECTION_OPS_TYPE,
|
||||
isSharedCollection,
|
||||
handleCollectionOps,
|
||||
getSelectedCollection,
|
||||
isFavoriteCollection,
|
||||
getArchivedCollections,
|
||||
hasNonSystemCollections,
|
||||
} from 'utils/collection';
|
||||
|
@ -87,7 +88,7 @@ import FixCreationTime, {
|
|||
} from 'components/FixCreationTime';
|
||||
import { Collection, CollectionSummaries } from 'types/collection';
|
||||
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 Collections from 'components/Collections';
|
||||
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
|
||||
|
@ -124,60 +125,18 @@ export const GalleryContext = createContext<GalleryContextType>(
|
|||
defaultGalleryContext
|
||||
);
|
||||
|
||||
type FilesFn = EnteFile[] | ((files: EnteFile[]) => EnteFile[]);
|
||||
|
||||
export default function Gallery() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState(null);
|
||||
const [collections, setCollections] = useState<Collection[]>(null);
|
||||
const [files, setFilesOriginal] = 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 [files, setFiles] = useState<EnteFile[]>(null);
|
||||
|
||||
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
||||
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(false);
|
||||
const [isFirstFetch, setIsFirstFetch] = useState(false);
|
||||
const [selected, setSelected] = useState<SelectedState>({
|
||||
ownCount: 0,
|
||||
count: 0,
|
||||
collectionID: 0,
|
||||
});
|
||||
|
@ -326,6 +285,8 @@ export default function Gallery() {
|
|||
collectionURL += constants.ARCHIVE;
|
||||
} else if (activeCollection === TRASH_SECTION) {
|
||||
collectionURL += constants.TRASH;
|
||||
} else if (activeCollection === DUMMY_UNCATEGORIZED_SECTION) {
|
||||
collectionURL += UNCATEGORIZED_COLLECTION_NAME;
|
||||
} else {
|
||||
collectionURL += activeCollection;
|
||||
}
|
||||
|
@ -373,10 +334,8 @@ export default function Gallery() {
|
|||
!silent && startLoading();
|
||||
const collections = await syncCollections();
|
||||
setCollections(collections);
|
||||
let files = await syncFiles(collections, setFiles);
|
||||
const trash = await syncTrash(collections, setFiles, files);
|
||||
files = [...files, ...getTrashedFiles(trash)];
|
||||
setFiles(sortFiles(files));
|
||||
await syncFiles(collections, setFiles);
|
||||
await syncTrash(collections, setFiles);
|
||||
} catch (e) {
|
||||
logError(e, 'syncWithRemote failed');
|
||||
switch (e.message) {
|
||||
|
@ -389,6 +348,7 @@ export default function Gallery() {
|
|||
break;
|
||||
}
|
||||
} finally {
|
||||
setDeletedFileIds(new Set());
|
||||
!silent && finishLoading();
|
||||
}
|
||||
syncInProgress.current = false;
|
||||
|
@ -408,7 +368,7 @@ export default function Gallery() {
|
|||
const archivedCollections = getArchivedCollections(collections);
|
||||
setArchivedCollections(archivedCollections);
|
||||
|
||||
const collectionSummaries = getCollectionSummaries(
|
||||
const collectionSummaries = await getCollectionSummaries(
|
||||
user,
|
||||
collections,
|
||||
files,
|
||||
|
@ -418,7 +378,7 @@ export default function Gallery() {
|
|||
};
|
||||
|
||||
const clearSelection = function () {
|
||||
setSelected({ count: 0, collectionID: 0 });
|
||||
setSelected({ ownCount: 0, count: 0, collectionID: 0 });
|
||||
};
|
||||
|
||||
if (!files || !collectionSummaries) {
|
||||
|
@ -430,10 +390,19 @@ export default function Gallery() {
|
|||
try {
|
||||
setCollectionSelectorView(false);
|
||||
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(
|
||||
ops,
|
||||
collection,
|
||||
selectedFiles,
|
||||
toProcessFiles,
|
||||
selected.collectionID
|
||||
);
|
||||
clearSelection();
|
||||
|
@ -551,7 +520,6 @@ export default function Gallery() {
|
|||
});
|
||||
} finally {
|
||||
await syncWithRemote(false, true);
|
||||
setDeletedFileIds(new Set());
|
||||
finishLoading();
|
||||
}
|
||||
};
|
||||
|
@ -724,10 +692,10 @@ export default function Gallery() {
|
|||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
activeCollection={activeCollection}
|
||||
isSharedCollection={isSharedCollection(
|
||||
activeCollection,
|
||||
collections
|
||||
)}
|
||||
isIncomingSharedCollection={
|
||||
collectionSummaries.get(activeCollection)?.type ===
|
||||
CollectionSummaryType.incomingShare
|
||||
}
|
||||
enableDownload={true}
|
||||
resetSearch={resetSearch}
|
||||
/>
|
||||
|
@ -771,12 +739,23 @@ export default function Gallery() {
|
|||
fixTimeHelper={fixTimeHelper}
|
||||
downloadHelper={downloadHelper}
|
||||
count={selected.count}
|
||||
ownCount={selected.ownCount}
|
||||
clearSelection={clearSelection}
|
||||
activeCollection={activeCollection}
|
||||
isFavoriteCollection={isFavoriteCollection(
|
||||
activeCollection,
|
||||
collections
|
||||
)}
|
||||
isFavoriteCollection={
|
||||
collectionSummaries.get(activeCollection)
|
||||
?.type === CollectionSummaryType.favorites
|
||||
}
|
||||
isUncategorizedCollection={
|
||||
collectionSummaries.get(activeCollection)
|
||||
?.type ===
|
||||
CollectionSummaryType.uncategorized
|
||||
}
|
||||
isIncomingSharedCollection={
|
||||
collectionSummaries.get(activeCollection)
|
||||
?.type ===
|
||||
CollectionSummaryType.incomingShare
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</FullScreenDropZone>
|
||||
|
|
|
@ -407,10 +407,10 @@ export default function PublicCollectionGallery() {
|
|||
files={publicFiles}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={() => null}
|
||||
selected={{ count: 0, collectionID: null }}
|
||||
selected={{ count: 0, collectionID: null, ownCount: 0 }}
|
||||
isFirstLoad={true}
|
||||
activeCollection={ALL_SECTION}
|
||||
isSharedCollection
|
||||
isIncomingSharedCollection
|
||||
enableDownload={
|
||||
publicCollection?.publicURLs?.[0]?.enableDownload ??
|
||||
true
|
||||
|
|
|
@ -19,7 +19,6 @@ import {
|
|||
AddToCollectionRequest,
|
||||
MoveToCollectionRequest,
|
||||
EncryptedFileKey,
|
||||
RemoveFromCollectionRequest,
|
||||
CreatePublicAccessTokenRequest,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
|
@ -29,6 +28,7 @@ import {
|
|||
EncryptedCollection,
|
||||
CollectionMagicMetadata,
|
||||
CollectionMagicMetadataProps,
|
||||
RemoveFromCollectionRequest,
|
||||
} from 'types/collection';
|
||||
import {
|
||||
COLLECTION_SORT_BY,
|
||||
|
@ -38,6 +38,9 @@ import {
|
|||
COLLECTION_SORT_ORDER,
|
||||
ALL_SECTION,
|
||||
CollectionSummaryType,
|
||||
UNCATEGORIZED_COLLECTION_NAME,
|
||||
FAVORITE_COLLECTION_NAME,
|
||||
DUMMY_UNCATEGORIZED_SECTION,
|
||||
} from 'constants/collection';
|
||||
import {
|
||||
NEW_COLLECTION_MAGIC_METADATA,
|
||||
|
@ -53,8 +56,10 @@ import {
|
|||
isOutgoingShare,
|
||||
isIncomingShare,
|
||||
isSharedOnlyViaLink,
|
||||
isValidMoveTarget,
|
||||
} from 'utils/collection';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import { getLocalFiles } from './fileService';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const COLLECTION_TABLE = 'collections';
|
||||
|
@ -238,7 +243,8 @@ export const getCollection = async (
|
|||
);
|
||||
return collectionWithSecrets;
|
||||
} 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();
|
||||
if (!favCollection) {
|
||||
favCollection = await createCollection(
|
||||
'Favorites',
|
||||
FAVORITE_COLLECTION_NAME,
|
||||
CollectionType.favorites
|
||||
);
|
||||
const localCollections = await getLocalCollections();
|
||||
|
@ -491,35 +497,142 @@ const encryptWithNewCollectionKey = async (
|
|||
};
|
||||
export const removeFromCollection = async (
|
||||
collectionID: number,
|
||||
files: EnteFile[]
|
||||
toRemoveFiles: EnteFile[],
|
||||
allFiles?: EnteFile[]
|
||||
) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
const request: RemoveFromCollectionRequest = {
|
||||
collectionID: collectionID,
|
||||
fileIDs: files.map((file) => file.id),
|
||||
};
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
const nonUserFiles = [];
|
||||
const userFiles = [];
|
||||
for (const file of toRemoveFiles) {
|
||||
if (file.ownerID === user.id) {
|
||||
userFiles.push(file);
|
||||
} else {
|
||||
nonUserFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
await HTTPService.post(
|
||||
`${ENDPOINT}/collections/v2/remove-files`,
|
||||
request,
|
||||
null,
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
if (nonUserFiles.length > 0) {
|
||||
await removeNonUserFiles(collectionID, nonUserFiles);
|
||||
}
|
||||
if (userFiles.length > 0) {
|
||||
await removeUserFiles(collectionID, userFiles, allFiles);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'remove from collection failed ');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCollection = async (collectionID: number) => {
|
||||
export const removeUserFiles = async (
|
||||
sourceCollectionID: number,
|
||||
toRemoveFiles: EnteFile[],
|
||||
allFiles?: EnteFile[]
|
||||
) => {
|
||||
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();
|
||||
|
||||
await HTTPService.delete(
|
||||
`${ENDPOINT}/collections/v2/${collectionID}`,
|
||||
null,
|
||||
`${ENDPOINT}/collections/v3/${collectionID}`,
|
||||
null,
|
||||
{ collectionID, keepFiles },
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
} catch (e) {
|
||||
|
@ -816,12 +929,12 @@ function compareCollectionsLatestFile(first: EnteFile, second: EnteFile) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getCollectionSummaries(
|
||||
export async function getCollectionSummaries(
|
||||
user: User,
|
||||
collections: Collection[],
|
||||
files: EnteFile[],
|
||||
archivedCollections: Set<number>
|
||||
): CollectionSummaries {
|
||||
): Promise<CollectionSummaries> {
|
||||
const collectionSummaries: CollectionSummaries = new Map();
|
||||
const collectionLatestFiles = getCollectionLatestFiles(
|
||||
files,
|
||||
|
@ -833,12 +946,15 @@ export function getCollectionSummaries(
|
|||
);
|
||||
|
||||
for (const collection of collections) {
|
||||
if (collectionFilesCount.get(collection.id)) {
|
||||
if (
|
||||
collectionFilesCount.get(collection.id) ||
|
||||
collection.type === CollectionType.uncategorized
|
||||
) {
|
||||
collectionSummaries.set(collection.id, {
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
latestFile: collectionLatestFiles.get(collection.id),
|
||||
fileCount: collectionFilesCount.get(collection.id),
|
||||
fileCount: collectionFilesCount.get(collection.id) ?? 0,
|
||||
updationTime: collection.updationTime,
|
||||
type: isIncomingShare(collection, user)
|
||||
? CollectionSummaryType.incomingShare
|
||||
|
@ -852,6 +968,16 @@ export function getCollectionSummaries(
|
|||
});
|
||||
}
|
||||
}
|
||||
const uncategorizedCollection = await getUncategorizedCollection(
|
||||
collections
|
||||
);
|
||||
|
||||
if (!uncategorizedCollection) {
|
||||
collectionSummaries.set(
|
||||
DUMMY_UNCATEGORIZED_SECTION,
|
||||
getDummyUncategorizedCollectionSummaries()
|
||||
);
|
||||
}
|
||||
collectionSummaries.set(
|
||||
ALL_SECTION,
|
||||
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(
|
||||
collectionFilesCount: CollectionFilesCount,
|
||||
collectionsLatestFile: CollectionLatestFiles
|
||||
|
@ -946,3 +1083,23 @@ function getTrashedCollectionSummaries(
|
|||
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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ElectronAPIs } from 'types/electron';
|
||||
import { ElectronFile } from 'types/upload';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
@ -37,7 +38,12 @@ class ElectronImageProcessorService {
|
|||
);
|
||||
return new Blob([convertedFileData]);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +74,12 @@ class ElectronImageProcessorService {
|
|||
);
|
||||
return thumb;
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import HTTPService from './HTTPService';
|
|||
import { logError } from 'utils/sentry';
|
||||
import {
|
||||
decryptFile,
|
||||
getLatestVersionFiles,
|
||||
mergeMetadata,
|
||||
preservePhotoswipeProps,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
import { EnteFile, EncryptedEnteFile, TrashRequest } from 'types/file';
|
||||
|
@ -54,12 +54,12 @@ const setLocalFiles = async (files: EnteFile[]) => {
|
|||
export const syncFiles = async (
|
||||
collections: Collection[],
|
||||
setFiles: SetFiles
|
||||
) => {
|
||||
): Promise<EnteFile[]> => {
|
||||
const localFiles = await getLocalFiles();
|
||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||
if (files.length !== localFiles.length) {
|
||||
await setLocalFiles(files);
|
||||
setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))]));
|
||||
setFiles(sortFiles(mergeMetadata(files)));
|
||||
}
|
||||
for (const collection of collections) {
|
||||
if (!getToken()) {
|
||||
|
@ -72,38 +72,17 @@ export const syncFiles = async (
|
|||
if (collection.updationTime === lastSyncTime) {
|
||||
continue;
|
||||
}
|
||||
const fetchedFiles =
|
||||
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? [];
|
||||
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);
|
||||
}
|
||||
const newFiles = await getFiles(collection, lastSyncTime, setFiles);
|
||||
files = getLatestVersionFiles([...files, ...newFiles]);
|
||||
await setLocalFiles(files);
|
||||
setCollectionLastSyncTime(collection, collection.updationTime);
|
||||
setFiles(preservePhotoswipeProps([...sortFiles(mergeMetadata(files))]));
|
||||
}
|
||||
return sortFiles(mergeMetadata(files));
|
||||
return files;
|
||||
};
|
||||
|
||||
export const getFiles = async (
|
||||
collection: Collection,
|
||||
sinceTime: number,
|
||||
files: EnteFile[],
|
||||
setFiles: SetFiles
|
||||
): Promise<EnteFile[]> => {
|
||||
try {
|
||||
|
@ -126,37 +105,35 @@ export const getFiles = async (
|
|||
}
|
||||
);
|
||||
|
||||
decryptedFiles = [
|
||||
...decryptedFiles,
|
||||
...(await Promise.all(
|
||||
resp.data.diff.map(async (file: EncryptedEnteFile) => {
|
||||
if (!file.isDeleted) {
|
||||
return await decryptFile(file, collection.key);
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}) as Promise<EnteFile>[]
|
||||
)),
|
||||
];
|
||||
const newDecryptedFilesBatch = await Promise.all(
|
||||
resp.data.diff.map(async (file: EncryptedEnteFile) => {
|
||||
if (!file.isDeleted) {
|
||||
return await decryptFile(file, collection.key);
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}) as Promise<EnteFile>[]
|
||||
);
|
||||
decryptedFiles = [...decryptedFiles, ...newDecryptedFilesBatch];
|
||||
|
||||
if (resp.data.diff.length) {
|
||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||
}
|
||||
setFiles(
|
||||
preservePhotoswipeProps(
|
||||
sortFiles(
|
||||
mergeMetadata(
|
||||
[...(files || []), ...decryptedFiles].filter(
|
||||
(item) => !item.isDeleted
|
||||
)
|
||||
)
|
||||
setFiles((files) =>
|
||||
sortFiles(
|
||||
mergeMetadata(
|
||||
getLatestVersionFiles([
|
||||
...(files || []),
|
||||
...decryptedFiles,
|
||||
])
|
||||
)
|
||||
)
|
||||
);
|
||||
if (resp.data.diff.length) {
|
||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||
}
|
||||
} while (resp.data.hasMore);
|
||||
return decryptedFiles;
|
||||
} catch (e) {
|
||||
logError(e, 'Get files failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -2,16 +2,10 @@ import { SetFiles } from 'types/gallery';
|
|||
import { Collection } from 'types/collection';
|
||||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import {
|
||||
decryptFile,
|
||||
mergeMetadata,
|
||||
preservePhotoswipeProps,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import localForage from 'utils/storage/localForage';
|
||||
import { getCollection } from './collectionService';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
import HTTPService from './HTTPService';
|
||||
import { EncryptedTrashItem, Trash } from 'types/trash';
|
||||
|
@ -30,7 +24,13 @@ export async function getLocalTrash() {
|
|||
export async function getLocalDeletedCollections() {
|
||||
const trashedCollections: Array<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) {
|
||||
|
@ -49,16 +49,15 @@ async function getLastSyncTime() {
|
|||
}
|
||||
export async function syncTrash(
|
||||
collections: Collection[],
|
||||
setFiles: SetFiles,
|
||||
files: EnteFile[]
|
||||
): Promise<Trash> {
|
||||
setFiles: SetFiles
|
||||
): Promise<void> {
|
||||
const trash = await getLocalTrash();
|
||||
collections = [...collections, ...(await getLocalDeletedCollections())];
|
||||
const collectionMap = new Map<number, Collection>(
|
||||
collections.map((collection) => [collection.id, collection])
|
||||
);
|
||||
if (!getToken()) {
|
||||
return trash;
|
||||
return;
|
||||
}
|
||||
const lastSyncTime = await getLastSyncTime();
|
||||
|
||||
|
@ -66,18 +65,15 @@ export async function syncTrash(
|
|||
collectionMap,
|
||||
lastSyncTime,
|
||||
setFiles,
|
||||
files,
|
||||
trash
|
||||
);
|
||||
cleanTrashCollections(updatedTrash);
|
||||
return updatedTrash;
|
||||
}
|
||||
|
||||
export const updateTrash = async (
|
||||
collections: Map<number, Collection>,
|
||||
sinceTime: number,
|
||||
setFiles: SetFiles,
|
||||
files: EnteFile[],
|
||||
currentTrash: Trash
|
||||
): Promise<Trash> => {
|
||||
try {
|
||||
|
@ -127,16 +123,8 @@ export const updateTrash = async (
|
|||
time = resp.data.diff.slice(-1)[0].updatedAt;
|
||||
}
|
||||
|
||||
setFiles(
|
||||
preservePhotoswipeProps(
|
||||
sortFiles([
|
||||
...(files ?? []).map((file) => ({
|
||||
...file,
|
||||
isTrashed: false,
|
||||
})),
|
||||
...getTrashedFiles(updatedTrash),
|
||||
])
|
||||
)
|
||||
setFiles((files) =>
|
||||
sortFiles([...(files ?? []), ...getTrashedFiles(updatedTrash)])
|
||||
);
|
||||
await localForage.setItem(TRASH, updatedTrash);
|
||||
await localForage.setItem(TRASH_TIME, time);
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { FILE_TYPE } from 'constants/file';
|
||||
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 { getFileExtension } from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
@ -27,7 +30,7 @@ export async function getFileType(
|
|||
const mimTypeParts: string[] = typeResult.mime?.split('/');
|
||||
|
||||
if (mimTypeParts?.length !== 2) {
|
||||
throw Error(CustomError.TYPE_DETECTION_FAILED);
|
||||
throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime));
|
||||
}
|
||||
switch (mimTypeParts[0]) {
|
||||
case TYPE_IMAGE:
|
||||
|
@ -37,7 +40,7 @@ export async function getFileType(
|
|||
fileType = FILE_TYPE.VIDEO;
|
||||
break;
|
||||
default:
|
||||
fileType = FILE_TYPE.OTHERS;
|
||||
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
|
||||
}
|
||||
return {
|
||||
fileType,
|
||||
|
@ -45,6 +48,9 @@ export async function getFileType(
|
|||
mimeType: typeResult.mime,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e.message === CustomError.UNSUPPORTED_FILE_FORMAT) {
|
||||
throw e;
|
||||
}
|
||||
const fileFormat = getFileExtension(receivedFile.name);
|
||||
const formatMissedByTypeDetection = FILE_TYPE_LIB_MISSED_FORMATS.find(
|
||||
(a) => a.exactType === fileFormat
|
||||
|
@ -52,14 +58,13 @@ export async function getFileType(
|
|||
if (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,
|
||||
});
|
||||
return {
|
||||
fileType: FILE_TYPE.OTHERS,
|
||||
exactType: fileFormat,
|
||||
mimeType: receivedFile instanceof File ? receivedFile.type : null,
|
||||
};
|
||||
throw Error(CustomError.TYPE_DETECTION_FAILED(fileFormat));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,18 +83,15 @@ async function extractElectronFileType(file: ElectronFile) {
|
|||
}
|
||||
|
||||
async function getFileTypeFromBuffer(buffer: Uint8Array) {
|
||||
try {
|
||||
const result = await FileType.fromBuffer(buffer);
|
||||
if (!result.mime) {
|
||||
logError(
|
||||
Error('mimetype missing from file type result'),
|
||||
CustomError.TYPE_DETECTION_FAILED,
|
||||
{ result }
|
||||
);
|
||||
throw Error(CustomError.TYPE_DETECTION_FAILED);
|
||||
const result = await FileType.fromBuffer(buffer);
|
||||
if (!result?.mime) {
|
||||
let logableInfo = '';
|
||||
try {
|
||||
logableInfo = `result: ${JSON.stringify(result)}`;
|
||||
} catch (e) {
|
||||
logableInfo = 'failed to stringify result';
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
throw Error(CustomError.TYPE_DETECTION_FAILED);
|
||||
throw Error(`mimetype missing from file type result - ${logableInfo}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -88,13 +88,6 @@ async function generateImageThumbnail(
|
|||
MAX_THUMBNAIL_SIZE
|
||||
);
|
||||
} catch (e) {
|
||||
logError(
|
||||
e,
|
||||
'Error generating thumbnail using electron image processor',
|
||||
{
|
||||
fileFormat: fileTypeInfo.exactType,
|
||||
}
|
||||
);
|
||||
return await generateImageThumbnailUsingCanvas(file, fileTypeInfo);
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -2,7 +2,6 @@ import { getLocalFiles } from '../fileService';
|
|||
import { SetFiles } from 'types/gallery';
|
||||
import {
|
||||
sortFiles,
|
||||
preservePhotoswipeProps,
|
||||
decryptFile,
|
||||
getUserOwnedNonTrashedFiles,
|
||||
} from 'utils/file';
|
||||
|
@ -414,7 +413,7 @@ class UploadManager {
|
|||
private updateUIFiles(decryptedFile: EnteFile) {
|
||||
this.existingFiles.push(decryptedFile);
|
||||
this.existingFiles = sortFiles(this.existingFiles);
|
||||
this.setFiles(preservePhotoswipeProps(this.existingFiles));
|
||||
this.setFiles((files) => sortFiles([...files, decryptedFile]));
|
||||
}
|
||||
|
||||
private updateElectronRemainingFiles(
|
||||
|
|
|
@ -51,10 +51,9 @@ export default async function uploader(
|
|||
}
|
||||
addLogLine(`getting filetype for ${fileNameSize}`);
|
||||
fileTypeInfo = await UploadService.getAssetFileType(uploadAsset);
|
||||
addLogLine(`got filetype for ${fileNameSize}`);
|
||||
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
|
||||
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
|
||||
}
|
||||
addLogLine(
|
||||
`got filetype for ${fileNameSize} - ${JSON.stringify(fileTypeInfo)}`
|
||||
);
|
||||
if (skipVideos && fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
|
||||
addLogLine(
|
||||
`skipped video upload for public upload ${fileNameSize}`
|
||||
|
@ -176,7 +175,10 @@ export default async function uploader(
|
|||
};
|
||||
} catch (e) {
|
||||
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', {
|
||||
fileFormat: fileTypeInfo?.exactType,
|
||||
});
|
||||
|
|
|
@ -18,12 +18,12 @@ import {
|
|||
UserDetails,
|
||||
DeleteChallengeResponse,
|
||||
} from 'types/user';
|
||||
import { getLocalFamilyData, isPartOfFamily } from 'utils/billing';
|
||||
import { ServerErrorCodes } from 'utils/error';
|
||||
import isElectron from 'is-electron';
|
||||
import safeStorageService from './electron/safeStorage';
|
||||
import { deleteThumbnailCache } from './cacheService';
|
||||
import { B64EncryptionResult } from 'types/crypto';
|
||||
import { isPartOfFamily, getLocalFamilyData } from 'utils/user/family';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
|
|
|
@ -37,7 +37,6 @@ export interface Collection
|
|||
> {
|
||||
key: string;
|
||||
name: string;
|
||||
isSharedCollection?: boolean;
|
||||
magicMetadata: CollectionMagicMetadata;
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,12 @@ export interface DialogBoxAttributes {
|
|||
variant: ButtonProps['color'];
|
||||
disabled?: boolean;
|
||||
};
|
||||
secondary?: {
|
||||
text: string;
|
||||
action: () => void;
|
||||
variant: ButtonProps['color'];
|
||||
disabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type SetDialogBoxAttributes = React.Dispatch<
|
||||
|
|
|
@ -5,6 +5,7 @@ import { EnteFile } from 'types/file';
|
|||
|
||||
export type SelectedState = {
|
||||
[k: number]: boolean;
|
||||
ownCount: number;
|
||||
count: number;
|
||||
collectionID: number;
|
||||
};
|
||||
|
@ -15,9 +16,14 @@ export type SetCollectionSelectorAttributes = React.Dispatch<
|
|||
React.SetStateAction<CollectionSelectorAttributes>
|
||||
>;
|
||||
|
||||
export type MergedSourceURL = {
|
||||
original: string;
|
||||
converted: string;
|
||||
};
|
||||
|
||||
export type GalleryContextType = {
|
||||
thumbs: Map<number, string>;
|
||||
files: Map<number, { original: string; converted: string }>;
|
||||
files: Map<number, MergedSourceURL>;
|
||||
showPlanSelectorModal: () => void;
|
||||
setActiveCollection: (collection: number) => void;
|
||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
|
|
|
@ -8,8 +8,9 @@ import { CustomError } from '../error';
|
|||
import { logError } from '../sentry';
|
||||
import { SetDialogBoxAttributes } from 'types/dialogBox';
|
||||
import { getFamilyPortalRedirectURL } from 'services/userService';
|
||||
import { FamilyData, FamilyMember, User, UserDetails } from 'types/user';
|
||||
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_APPSTORE = 'appstore';
|
||||
|
@ -97,52 +98,10 @@ export function isSubscriptionCancelled(subscription: Subscription) {
|
|||
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 {
|
||||
return getData(LS_KEYS.SUBSCRIPTION);
|
||||
}
|
||||
|
||||
export function getLocalFamilyData(): FamilyData {
|
||||
return getData(LS_KEYS.FAMILY_DATA);
|
||||
}
|
||||
|
||||
export function isUserSubscribedPlan(plan: Plan, subscription: Subscription) {
|
||||
return (
|
||||
isSubscriptionActive(subscription) &&
|
||||
|
|
|
@ -20,7 +20,6 @@ import {
|
|||
} from 'types/collection';
|
||||
import {
|
||||
CollectionSummaryType,
|
||||
CollectionType,
|
||||
HIDE_FROM_COLLECTION_BAR_TYPES,
|
||||
OPTIONS_NOT_HAVING_COLLECTION_TYPES,
|
||||
SYSTEM_COLLECTION_TYPES,
|
||||
|
@ -77,31 +76,6 @@ export function getSelectedCollection(
|
|||
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) {
|
||||
try {
|
||||
const allFiles = await getLocalFiles();
|
||||
|
@ -204,7 +178,10 @@ export const getArchivedCollections = (collections: Collection[]) => {
|
|||
export const hasNonSystemCollections = (
|
||||
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) => {
|
||||
|
@ -251,3 +228,16 @@ export function isIncomingShare(collection: Collection, user: User) {
|
|||
export function isSharedOnlyViaLink(collection: Collection) {
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,44 +11,49 @@ export const ServerErrorCodes = {
|
|||
NOT_FOUND: '404',
|
||||
};
|
||||
|
||||
export enum CustomError {
|
||||
SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed',
|
||||
THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed',
|
||||
VIDEO_PLAYBACK_FAILED = 'video playback failed',
|
||||
ETAG_MISSING = 'no header/etag present in response body',
|
||||
KEY_MISSING = 'encrypted key missing from localStorage',
|
||||
FAILED_TO_LOAD_WEB_WORKER = 'failed to load web worker',
|
||||
CHUNK_MORE_THAN_EXPECTED = 'chunks more than expected',
|
||||
CHUNK_LESS_THAN_EXPECTED = 'chunks less than expected',
|
||||
UNSUPPORTED_FILE_FORMAT = 'unsupported file formats',
|
||||
FILE_TOO_LARGE = 'file too large',
|
||||
SUBSCRIPTION_EXPIRED = 'subscription expired',
|
||||
STORAGE_QUOTA_EXCEEDED = 'storage quota exceeded',
|
||||
SESSION_EXPIRED = 'session expired',
|
||||
TYPE_DETECTION_FAILED = 'type detection failed',
|
||||
SIGNUP_FAILED = 'signup failed',
|
||||
FAV_COLLECTION_MISSING = 'favorite collection missing',
|
||||
INVALID_COLLECTION_OPERATION = 'invalid collection operation',
|
||||
WAIT_TIME_EXCEEDED = 'thumbnail generation wait time exceeded',
|
||||
REQUEST_CANCELLED = 'request canceled',
|
||||
REQUEST_FAILED = 'request failed',
|
||||
TOKEN_EXPIRED = 'token expired',
|
||||
TOKEN_MISSING = 'token missing',
|
||||
TOO_MANY_REQUESTS = 'too many requests',
|
||||
BAD_REQUEST = 'bad request',
|
||||
SUBSCRIPTION_NEEDED = 'subscription not present',
|
||||
NOT_FOUND = 'not found ',
|
||||
NO_METADATA = 'no metadata',
|
||||
TOO_LARGE_LIVE_PHOTO_ASSETS = 'too large live photo assets',
|
||||
NOT_A_DATE = 'not a date',
|
||||
FILE_ID_NOT_FOUND = 'file with id not found',
|
||||
WEAK_DEVICE = 'password decryption failed on the device',
|
||||
INCORRECT_PASSWORD = 'incorrect password',
|
||||
UPLOAD_CANCELLED = 'upload cancelled',
|
||||
REQUEST_TIMEOUT = 'request taking too long',
|
||||
HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED = 'hidden collection sync file attempted',
|
||||
UNKNOWN_ERROR = 'Something went wrong, please try again',
|
||||
}
|
||||
export const CustomError = {
|
||||
SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed',
|
||||
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
|
||||
VIDEO_PLAYBACK_FAILED: 'video playback failed',
|
||||
ETAG_MISSING: 'no header/etag present in response body',
|
||||
KEY_MISSING: 'encrypted key missing from localStorage',
|
||||
FAILED_TO_LOAD_WEB_WORKER: 'failed to load web worker',
|
||||
CHUNK_MORE_THAN_EXPECTED: 'chunks more than expected',
|
||||
CHUNK_LESS_THAN_EXPECTED: 'chunks less than expected',
|
||||
UNSUPPORTED_FILE_FORMAT: 'unsupported file format',
|
||||
FILE_TOO_LARGE: 'file too large',
|
||||
SUBSCRIPTION_EXPIRED: 'subscription expired',
|
||||
STORAGE_QUOTA_EXCEEDED: 'storage quota exceeded',
|
||||
SESSION_EXPIRED: 'session expired',
|
||||
INVALID_MIME_TYPE: (type: string) => `invalid mime type -${type}`,
|
||||
SIGNUP_FAILED: 'signup failed',
|
||||
FAV_COLLECTION_MISSING: 'favorite collection missing',
|
||||
INVALID_COLLECTION_OPERATION: 'invalid collection operation',
|
||||
WAIT_TIME_EXCEEDED: 'thumbnail generation wait time exceeded',
|
||||
REQUEST_CANCELLED: 'request canceled',
|
||||
REQUEST_FAILED: 'request failed',
|
||||
TOKEN_EXPIRED: 'token expired',
|
||||
TOKEN_MISSING: 'token missing',
|
||||
TOO_MANY_REQUESTS: 'too many requests',
|
||||
BAD_REQUEST: 'bad request',
|
||||
SUBSCRIPTION_NEEDED: 'subscription not present',
|
||||
NOT_FOUND: 'not found ',
|
||||
NO_METADATA: 'no metadata',
|
||||
TOO_LARGE_LIVE_PHOTO_ASSETS: 'too large live photo assets',
|
||||
NOT_A_DATE: 'not a date',
|
||||
FILE_ID_NOT_FOUND: 'file with id not found',
|
||||
WEAK_DEVICE: 'password decryption failed on the device',
|
||||
INCORRECT_PASSWORD: 'incorrect password',
|
||||
UPLOAD_CANCELLED: 'upload cancelled',
|
||||
REQUEST_TIMEOUT: 'request taking too long',
|
||||
HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED:
|
||||
'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) {
|
||||
let parsedMessage = null;
|
||||
|
|
|
@ -166,31 +166,14 @@ export function getSelectedFiles(
|
|||
}
|
||||
|
||||
export function sortFiles(files: EnteFile[]) {
|
||||
// sort according to modification time first
|
||||
files = files.sort((a, b) => {
|
||||
if (!b.metadata?.modificationTime) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.metadata?.modificationTime) {
|
||||
return 1;
|
||||
} else {
|
||||
// sort based on the time of creation time of the file,
|
||||
// for files with same creation time, sort based on the time of last modification
|
||||
return files.sort((a, b) => {
|
||||
if (a.metadata.creationTime === b.metadata.creationTime) {
|
||||
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(
|
||||
|
@ -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) {
|
||||
const lastDotPosition = filename.lastIndexOf('.');
|
||||
if (lastDotPosition === -1) return filename;
|
||||
|
@ -456,23 +426,16 @@ export function isSharedFile(user: User, file: EnteFile) {
|
|||
}
|
||||
|
||||
export function mergeMetadata(files: EnteFile[]): EnteFile[] {
|
||||
return files.map((file) => ({
|
||||
...file,
|
||||
metadata: {
|
||||
...file.metadata,
|
||||
...(file.pubMagicMetadata?.data
|
||||
? {
|
||||
...(file.pubMagicMetadata?.data.editedTime && {
|
||||
creationTime: file.pubMagicMetadata.data.editedTime,
|
||||
}),
|
||||
...(file.pubMagicMetadata?.data.editedName && {
|
||||
title: file.pubMagicMetadata.data.editedName,
|
||||
}),
|
||||
}
|
||||
: {}),
|
||||
...(file.magicMetadata?.data ? file.magicMetadata.data : {}),
|
||||
},
|
||||
}));
|
||||
return files.map((file) => {
|
||||
if (file.pubMagicMetadata?.data.editedTime) {
|
||||
file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
|
||||
}
|
||||
if (file.pubMagicMetadata?.data.editedName) {
|
||||
file.metadata.title = file.pubMagicMetadata.data.editedName;
|
||||
}
|
||||
|
||||
return file;
|
||||
});
|
||||
}
|
||||
|
||||
export function updateExistingFilePubMetadata(
|
||||
|
@ -592,3 +555,19 @@ export const copyFileToClipboard = async (fileUrl: string) => {
|
|||
.write([new ClipboardItem({ 'image/png': blobPromise })])
|
||||
.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
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
export async function isPlaybackPossible(url: string): Promise<boolean> {
|
||||
|
@ -32,3 +38,121 @@ export async function pauseVideo(livePhotoVideo, livePhotoImage) {
|
|||
livePhotoVideo.style.opacity = 0;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as Sentry from '@sentry/nextjs';
|
||||
import { isDEVSentryENV } from 'constants/sentry';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
|
||||
|
@ -17,12 +16,9 @@ export const logError = async (
|
|||
addLogLine(
|
||||
`error: ${error?.name} ${error?.message} ${
|
||||
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, {
|
||||
level: Sentry.Severity.Info,
|
||||
user: { id: await getSentryUserID() },
|
||||
|
|
|
@ -140,7 +140,8 @@ const englishConstants = {
|
|||
CREATE: 'Create',
|
||||
DOWNLOAD: 'Download',
|
||||
DOWNLOAD_OPTION: 'Download (D)',
|
||||
DOWNLOAD_FAVOURITES: 'Download favourites',
|
||||
DOWNLOAD_FAVORITES: 'Download favorites',
|
||||
DOWNLOAD_UNCATEGORIZED: 'Download uncategorized',
|
||||
COPY_OPTION: 'Copy as PNG (Ctrl/Cmd - C)',
|
||||
TOGGLE_FULLSCREEN: 'Toggle fullscreen (F)',
|
||||
ZOOM_IN_OUT: 'Zoom in/out',
|
||||
|
@ -368,8 +369,15 @@ const englishConstants = {
|
|||
DELETE_COLLECTION_TITLE: 'Delete album?',
|
||||
DELETE_COLLECTION: 'Delete album',
|
||||
DELETE_COLLECTION_FAILED: 'Album deletion failed, please try again',
|
||||
DELETE_COLLECTION_MESSAGE:
|
||||
'Files that are unique to this album will be moved to trash, and this album would be deleted.',
|
||||
DELETE_COLLECTION_MESSAGE: () => (
|
||||
<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_COLLECTION: 'Share album',
|
||||
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',
|
||||
ADD_TO_COLLECTION: 'Add to album',
|
||||
SELECTED: 'Selected',
|
||||
SELECTED: 'selected',
|
||||
VIDEO_PLAYBACK_FAILED: 'Video format not supported',
|
||||
VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD:
|
||||
'This video cannot be played on your browser',
|
||||
|
@ -587,6 +595,8 @@ const englishConstants = {
|
|||
THUMBNAIL_GENERATION_FAILED_INFO:
|
||||
'These files were uploaded, but unfortunately we could not generate the thumbnails for them.',
|
||||
UPLOAD_TO_COLLECTION: 'Upload to album',
|
||||
UNCATEGORIZED: 'Uncategorized',
|
||||
MOVE_TO_UNCATEGORIZED: 'Move to uncategorized',
|
||||
ARCHIVE: 'Archive',
|
||||
ARCHIVE_COLLECTION: 'Archive album',
|
||||
ARCHIVE_SECTION_NAME: 'Archive',
|
||||
|
@ -598,7 +608,9 @@ const englishConstants = {
|
|||
ADD: 'Add',
|
||||
SORT: 'Sort',
|
||||
REMOVE: 'Remove',
|
||||
YES_REMOVE: 'Yes, remove',
|
||||
CONFIRM_REMOVE: 'Confirm removal',
|
||||
REMOVE_FROM_COLLECTION: 'Remove from album',
|
||||
TRASH: 'Trash',
|
||||
MOVE_TO_TRASH: 'Move to trash',
|
||||
TRASH_FILES_MESSAGE:
|
||||
|
@ -620,11 +632,19 @@ const englishConstants = {
|
|||
LEAVE_SHARED_ALBUM_FAILED: 'failed to leave the album, please try again',
|
||||
LEAVE_SHARED_ALBUM_MESSAGE:
|
||||
'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>
|
||||
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>
|
||||
</>
|
||||
),
|
||||
|
@ -690,7 +710,7 @@ const englishConstants = {
|
|||
LINK_EXPIRED_MESSAGE: 'This link has either expired or been disabled!',
|
||||
MANAGE_LINK: 'Manage link',
|
||||
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:
|
||||
'Are you sure you want to disable public sharing?',
|
||||
FILE_DOWNLOAD: 'Allow downloads',
|
||||
|
@ -894,6 +914,7 @@ const englishConstants = {
|
|||
),
|
||||
ADD_X_PHOTOS: (x: number) => `Add ${x} ${x > 1 ? 'photos' : 'photo'}`,
|
||||
CHOSE_THEME: 'Choose theme',
|
||||
YOURS: 'yours',
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
45
src/utils/user/family.ts
Normal file
45
src/utils/user/family.ts
Normal 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
|
||||
);
|
||||
}
|
|
@ -2896,10 +2896,10 @@ jsx-ast-utils@^3.3.2:
|
|||
array-includes "^3.1.5"
|
||||
object.assign "^4.1.3"
|
||||
|
||||
jszip@3.7.1:
|
||||
version "3.7.1"
|
||||
resolved "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz"
|
||||
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
|
||||
jszip@3.8.0:
|
||||
version "3.8.0"
|
||||
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.8.0.tgz#a2ac3c33fe96a76489765168213655850254d51b"
|
||||
integrity sha512-cnpQrXvFSLdsR9KR5/x7zdf6c3m8IhZfZzSblFEHSqBaVwD2nvJ4CuCKLyvKvwBgZm08CgfSoiTBQLm5WW9hGw==
|
||||
dependencies:
|
||||
lie "~3.3.0"
|
||||
pako "~1.0.2"
|
||||
|
|
Loading…
Reference in a new issue