Merge branch 'master' into search-collection
This commit is contained in:
commit
4941cb514e
|
@ -57,6 +57,7 @@ export const Value = styled.div<{ width?: string }>`
|
|||
`;
|
||||
|
||||
export const FlexWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
|
|
@ -27,6 +27,8 @@ import {
|
|||
MIN_COLUMNS,
|
||||
SPACE_BTW_DATES,
|
||||
} from 'types';
|
||||
import { fileIsArchived } from 'utils/file';
|
||||
import { ALL_SECTION, ARCHIVE_SECTION } from './pages/gallery/Collections';
|
||||
import { isSharedFile } from 'utils/file';
|
||||
|
||||
const NO_OF_PAGES = 2;
|
||||
|
@ -404,12 +406,20 @@ const PhotoFrame = ({
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === ALL_SECTION && fileIsArchived(item)) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === ARCHIVE_SECTION && !fileIsArchived(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSharedFile(item) && !isSharedCollection) {
|
||||
return false;
|
||||
}
|
||||
if (!idSet.has(item.id)) {
|
||||
if (
|
||||
!activeCollection ||
|
||||
activeCollection === ALL_SECTION ||
|
||||
activeCollection === ARCHIVE_SECTION ||
|
||||
activeCollection === item.collectionID
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
|
|
|
@ -32,12 +32,11 @@ import InProgressIcon from './icons/InProgressIcon';
|
|||
import exportService from 'services/exportService';
|
||||
import { Subscription } from 'services/billingService';
|
||||
import { PAGES } from 'types';
|
||||
|
||||
import { ARCHIVE_SECTION } from 'components/pages/gallery/Collections';
|
||||
interface Props {
|
||||
collections: Collection[];
|
||||
setDialogMessage: SetDialogMessage;
|
||||
setLoading: SetLoading;
|
||||
showPlanSelectorModal: () => void;
|
||||
}
|
||||
export default function Sidebar(props: Props) {
|
||||
const [usage, SetUsage] = useState<string>(null);
|
||||
|
@ -52,7 +51,6 @@ export default function Sidebar(props: Props) {
|
|||
const [twoFactorModalView, setTwoFactorModalView] = useState(false);
|
||||
const [exportModalView, setExportModalView] = useState(false);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
galleryContext.showPlanSelectorModal = props.showPlanSelectorModal;
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (!isOpen) {
|
||||
|
@ -110,8 +108,19 @@ export default function Sidebar(props: Props) {
|
|||
const router = useRouter();
|
||||
function onManageClick() {
|
||||
setIsOpen(false);
|
||||
props.showPlanSelectorModal();
|
||||
galleryContext.showPlanSelectorModal();
|
||||
}
|
||||
|
||||
const Divider = () => (
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '40px',
|
||||
background: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<Menu
|
||||
isOpen={isOpen}
|
||||
|
@ -203,23 +212,14 @@ export default function Sidebar(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '40px',
|
||||
background: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={openFeedbackURL}>
|
||||
{constants.REQUEST_FEATURE}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => initiateEmail('contact@ente.io')}>
|
||||
{constants.SUPPORT}
|
||||
onClick={() => {
|
||||
galleryContext.setActiveCollection(ARCHIVE_SECTION);
|
||||
setIsOpen(false);
|
||||
}}>
|
||||
{constants.ARCHIVE}
|
||||
</LinkButton>
|
||||
<>
|
||||
<RecoveryKeyModal
|
||||
|
@ -266,6 +266,17 @@ export default function Sidebar(props: Props) {
|
|||
}}>
|
||||
{constants.UPDATE_EMAIL}
|
||||
</LinkButton>
|
||||
<Divider />
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={openFeedbackURL}>
|
||||
{constants.REQUEST_FEATURE}
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
style={{ marginTop: '30px' }}
|
||||
onClick={() => initiateEmail('contact@ente.io')}>
|
||||
{constants.SUPPORT}
|
||||
</LinkButton>
|
||||
<>
|
||||
<ExportModal
|
||||
show={exportModalView}
|
||||
|
@ -284,14 +295,7 @@ export default function Sidebar(props: Props) {
|
|||
</div>
|
||||
</LinkButton>
|
||||
</>
|
||||
<div
|
||||
style={{
|
||||
height: '1px',
|
||||
marginTop: '40px',
|
||||
background: '#242424',
|
||||
width: '100%',
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<LinkButton
|
||||
variant="danger"
|
||||
style={{ marginTop: '30px' }}
|
||||
|
|
20
src/components/icons/Archive.tsx
Normal file
20
src/components/icons/Archive.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function Archive(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill="currentColor">
|
||||
<path d="M10 3h4v5h3l-5 5-5-5h3v-5zm8.546 0h-2.344l5.467 9h-4.669l-2.25 3h-5.5l-2.25-3h-4.666l5.46-9h-2.317l-5.477 8.986v9.014h24v-9.014l-5.454-8.986z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
Archive.defaultProps = {
|
||||
height: 28,
|
||||
width: 20,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
20
src/components/icons/SortIcon.tsx
Normal file
20
src/components/icons/SortIcon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function SortIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
SortIcon.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
20
src/components/icons/TickIcon.tsx
Normal file
20
src/components/icons/TickIcon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function TickIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill="currentColor">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
TickIcon.defaultProps = {
|
||||
height: 28,
|
||||
width: 20,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
20
src/components/icons/UnArchive.tsx
Normal file
20
src/components/icons/UnArchive.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function UnArchive(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill="currentColor">
|
||||
<path d="M24 11.986v9.014h-24v-9.014l5.477-8.986h2.317l-5.46 9h4.666l2.25 3h5.5l2.25-3h4.669l-5.467-9h2.344l5.454 8.986zm-10-3.986h3l-5-5-5 5h3v5h4v-5zm-11.666 4" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
UnArchive.defaultProps = {
|
||||
height: 28,
|
||||
width: 20,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
|
@ -9,7 +9,7 @@ import {
|
|||
import { getSelectedCollection } from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import LinkButton from './LinkButton';
|
||||
import LinkButton, { ButtonVariant, LinkButtonProps } from './LinkButton';
|
||||
|
||||
interface Props {
|
||||
syncWithRemote: () => Promise<void>;
|
||||
|
@ -21,6 +21,28 @@ interface Props {
|
|||
showCollectionShareModal: () => void;
|
||||
redirectToAll: () => void;
|
||||
}
|
||||
|
||||
export const MenuLink = ({ children, ...props }: LinkButtonProps) => (
|
||||
<LinkButton
|
||||
style={{ fontSize: '14px', fontWeight: 700, padding: '8px 1em' }}
|
||||
{...props}>
|
||||
{children}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
export const MenuItem = (props: { children: any }) => (
|
||||
<ListGroup.Item
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#282828',
|
||||
padding: 0,
|
||||
}}>
|
||||
{props.children}
|
||||
</ListGroup.Item>
|
||||
);
|
||||
|
||||
const CollectionOptions = (props: Props) => {
|
||||
const collectionRename = async (
|
||||
selectedCollection: Collection,
|
||||
|
@ -75,23 +97,6 @@ const CollectionOptions = (props: Props) => {
|
|||
});
|
||||
};
|
||||
|
||||
const MenuLink = (props) => (
|
||||
<LinkButton
|
||||
style={{ fontSize: '14px', fontWeight: 700, padding: '8px 1em' }}
|
||||
{...props}>
|
||||
{props.children}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
const MenuItem = (props) => (
|
||||
<ListGroup.Item
|
||||
style={{
|
||||
background: '#282828',
|
||||
padding: 0,
|
||||
}}>
|
||||
{props.children}
|
||||
</ListGroup.Item>
|
||||
);
|
||||
return (
|
||||
<Popover id="collection-options" style={{ borderRadius: '10px' }}>
|
||||
<Popover.Content style={{ padding: 0, border: 'none' }}>
|
||||
|
@ -108,7 +113,7 @@ const CollectionOptions = (props: Props) => {
|
|||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuLink
|
||||
variant="danger"
|
||||
variant={ButtonVariant.danger}
|
||||
onClick={confirmDeleteCollection}>
|
||||
{constants.DELETE}
|
||||
</MenuLink>
|
||||
|
|
|
@ -7,6 +7,8 @@ import {
|
|||
} from 'services/collectionService';
|
||||
import AddCollectionButton from './AddCollectionButton';
|
||||
import PreviewCard from './PreviewCard';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { User } from 'services/userService';
|
||||
|
||||
export const CollectionIcon = styled.div`
|
||||
width: 200px;
|
||||
|
@ -53,14 +55,18 @@ function CollectionSelector({
|
|||
if (!attributes) {
|
||||
return;
|
||||
}
|
||||
const collectionsOtherThanFrom = collectionsAndTheirLatestFile?.filter(
|
||||
(item) => !(item.collection.id === attributes.fromCollection)
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
const personalCollectionsOtherThanFrom =
|
||||
collectionsAndTheirLatestFile?.filter(
|
||||
(item) =>
|
||||
item.collection.id !== attributes.fromCollection &&
|
||||
item.collection.owner.id === user?.id
|
||||
);
|
||||
if (collectionsOtherThanFrom.length === 0) {
|
||||
if (personalCollectionsOtherThanFrom.length === 0) {
|
||||
props.onHide();
|
||||
attributes.showNextModal();
|
||||
} else {
|
||||
setCollectionToShow(collectionsOtherThanFrom);
|
||||
setCollectionToShow(personalCollectionsOtherThanFrom);
|
||||
}
|
||||
}, [props.show]);
|
||||
|
||||
|
|
31
src/components/pages/gallery/CollectionSort.tsx
Normal file
31
src/components/pages/gallery/CollectionSort.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { IconButton } from 'components/Container';
|
||||
import SortIcon from 'components/icons/SortIcon';
|
||||
import React from 'react';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
import { COLLECTION_SORT_BY } from 'services/collectionService';
|
||||
import constants from 'utils/strings/constants';
|
||||
import CollectionSortOptions from './CollectionSortOptions';
|
||||
import { IconWithMessage } from './SelectedFileOptions';
|
||||
|
||||
interface Props {
|
||||
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;
|
||||
activeSortBy: COLLECTION_SORT_BY;
|
||||
}
|
||||
export default function CollectionSort(props: Props) {
|
||||
const collectionSortOptions = CollectionSortOptions(props);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
rootClose
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={collectionSortOptions}>
|
||||
<div>
|
||||
<IconWithMessage message={constants.SORT}>
|
||||
<IconButton style={{ color: '#fff' }}>
|
||||
<SortIcon />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
69
src/components/pages/gallery/CollectionSortOptions.tsx
Normal file
69
src/components/pages/gallery/CollectionSortOptions.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
import { Label, Value } from 'components/Container';
|
||||
import TickIcon from 'components/icons/TickIcon';
|
||||
import React from 'react';
|
||||
import { ListGroup, Popover, Row } from 'react-bootstrap';
|
||||
import { COLLECTION_SORT_BY } from 'services/collectionService';
|
||||
import styled from 'styled-components';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { MenuItem, MenuLink } from './CollectionOptions';
|
||||
|
||||
interface OptionProps {
|
||||
activeSortBy: COLLECTION_SORT_BY;
|
||||
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;
|
||||
}
|
||||
|
||||
const TickWrapper = styled.span`
|
||||
color: #aaa;
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const SortByOptionCreator =
|
||||
({ setCollectionSortBy, activeSortBy }: OptionProps) =>
|
||||
(props: { sortBy: COLLECTION_SORT_BY; children: any }) =>
|
||||
(
|
||||
<MenuItem>
|
||||
<Row>
|
||||
<Label width="20px">
|
||||
{activeSortBy === props.sortBy && (
|
||||
<TickWrapper>
|
||||
<TickIcon />
|
||||
</TickWrapper>
|
||||
)}
|
||||
</Label>
|
||||
<Value width="165px">
|
||||
<MenuLink
|
||||
onClick={() => setCollectionSortBy(props.sortBy)}
|
||||
variant={
|
||||
activeSortBy === props.sortBy && 'success'
|
||||
}>
|
||||
{props.children}
|
||||
</MenuLink>
|
||||
</Value>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const CollectionSortOptions = (props: OptionProps) => {
|
||||
const SortByOption = SortByOptionCreator(props);
|
||||
|
||||
return (
|
||||
<Popover id="collection-sort-options" style={{ borderRadius: '10px' }}>
|
||||
<Popover.Content
|
||||
style={{ padding: 0, border: 'none', width: '185px' }}>
|
||||
<ListGroup style={{ borderRadius: '8px' }}>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.LATEST_FILE}>
|
||||
{constants.SORT_BY_LATEST_PHOTO}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.MODIFICATION_TIME}>
|
||||
{constants.SORT_BY_MODIFICATION_TIME}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
|
||||
{constants.SORT_BY_COLLECTION_NAME}
|
||||
</SortByOption>
|
||||
</ListGroup>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionSortOptions;
|
|
@ -5,21 +5,31 @@ import NavigationButton, {
|
|||
} from 'components/NavigationButton';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { Collection, CollectionType } from 'services/collectionService';
|
||||
import {
|
||||
Collection,
|
||||
CollectionAndItsLatestFile,
|
||||
CollectionType,
|
||||
COLLECTION_SORT_BY,
|
||||
sortCollections,
|
||||
} from 'services/collectionService';
|
||||
import { User } from 'services/userService';
|
||||
import styled from 'styled-components';
|
||||
import { IMAGE_CONTAINER_MAX_WIDTH } from 'types';
|
||||
import { getSelectedCollection } from 'utils/collection';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import CollectionOptions from './CollectionOptions';
|
||||
import CollectionSort from './CollectionSort';
|
||||
import OptionIcon, { OptionIconWrapper } from './OptionIcon';
|
||||
|
||||
export const ARCHIVE_SECTION = -1;
|
||||
export const ALL_SECTION = 0;
|
||||
|
||||
interface CollectionProps {
|
||||
collections: Collection[];
|
||||
selected?: number;
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[];
|
||||
activeCollection?: number;
|
||||
setActiveCollection: (id?: number) => void;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
syncWithRemote: () => Promise<void>;
|
||||
|
@ -29,12 +39,11 @@ interface CollectionProps {
|
|||
collectionFilesCount: Map<number, number>;
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 10px auto;
|
||||
const CollectionContainer = styled.div`
|
||||
overflow-y: hidden;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
width: calc(100% - 80px);
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
|
||||
|
@ -52,6 +61,14 @@ const Wrapper = styled.div`
|
|||
scroll-behavior: smooth;
|
||||
`;
|
||||
|
||||
const CollectionBar = styled.div`
|
||||
width: 100%;
|
||||
margin: 10px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const Chip = styled.button<{ active: boolean }>`
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
|
@ -71,7 +88,7 @@ const Chip = styled.button<{ active: boolean }>`
|
|||
`;
|
||||
|
||||
export default function Collections(props: CollectionProps) {
|
||||
const { selected, collections, setActiveCollection } = props;
|
||||
const { activeCollection, collections, setActiveCollection } = props;
|
||||
const [selectedCollectionID, setSelectedCollectionID] =
|
||||
useState<number>(null);
|
||||
const collectionWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -89,6 +106,8 @@ export default function Collections(props: CollectionProps) {
|
|||
scrollWidth?: number;
|
||||
clientWidth?: number;
|
||||
}>({});
|
||||
const [collectionSortBy, setCollectionSortBy] =
|
||||
useState<COLLECTION_SORT_BY>(COLLECTION_SORT_BY.MODIFICATION_TIME);
|
||||
|
||||
const updateScrollObj = () => {
|
||||
if (collectionWrapperRef.current) {
|
||||
|
@ -110,10 +129,10 @@ export default function Collections(props: CollectionProps) {
|
|||
}, [collections]);
|
||||
|
||||
useEffect(() => {
|
||||
collectionChipsRef[selected]?.current.scrollIntoView({
|
||||
collectionChipsRef[activeCollection]?.current.scrollIntoView({
|
||||
inline: 'center',
|
||||
});
|
||||
}, [selected]);
|
||||
}, [activeCollection]);
|
||||
|
||||
const clickHandler = (collectionID?: number) => () => {
|
||||
setSelectedCollectionID(collectionID);
|
||||
|
@ -140,7 +159,7 @@ export default function Collections(props: CollectionProps) {
|
|||
const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
|
||||
collectionWrapperRef.current.scrollBy(250 * direction, 0);
|
||||
};
|
||||
const renderTooltip = (collectionID) => {
|
||||
const renderTooltip = (collectionID: number) => {
|
||||
const fileCount = props.collectionFilesCount?.get(collectionID) ?? 0;
|
||||
return (
|
||||
<Tooltip
|
||||
|
@ -176,20 +195,23 @@ export default function Collections(props: CollectionProps) {
|
|||
)}
|
||||
syncWithRemote={props.syncWithRemote}
|
||||
/>
|
||||
<Container>
|
||||
<CollectionBar>
|
||||
<CollectionContainer>
|
||||
{scrollObj.scrollLeft > 0 && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||
onClick={scrollCollection(
|
||||
SCROLL_DIRECTION.LEFT
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Wrapper
|
||||
ref={collectionWrapperRef}
|
||||
onScroll={updateScrollObj}>
|
||||
<Chip
|
||||
active={!selected}
|
||||
active={activeCollection === ALL_SECTION}
|
||||
onClick={clickHandler(ALL_SECTION)}>
|
||||
All
|
||||
{constants.ALL}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
|
@ -197,7 +219,11 @@ export default function Collections(props: CollectionProps) {
|
|||
}}
|
||||
/>
|
||||
</Chip>
|
||||
{collections?.map((item) => (
|
||||
{sortCollections(
|
||||
collections,
|
||||
props.collectionAndTheirLatestFile,
|
||||
collectionSortBy
|
||||
).map((item) => (
|
||||
<OverlayTrigger
|
||||
key={item.id}
|
||||
placement="top"
|
||||
|
@ -205,10 +231,11 @@ export default function Collections(props: CollectionProps) {
|
|||
overlay={renderTooltip(item.id)}>
|
||||
<Chip
|
||||
ref={collectionChipsRef[item.id]}
|
||||
active={selected === item.id}
|
||||
active={activeCollection === item.id}
|
||||
onClick={clickHandler(item.id)}>
|
||||
{item.name}
|
||||
{item.type !== CollectionType.favorites &&
|
||||
{item.type !==
|
||||
CollectionType.favorites &&
|
||||
item.owner.id === user?.id ? (
|
||||
<OverlayTrigger
|
||||
rootClose
|
||||
|
@ -234,15 +261,33 @@ export default function Collections(props: CollectionProps) {
|
|||
</Chip>
|
||||
</OverlayTrigger>
|
||||
))}
|
||||
<Chip
|
||||
active={activeCollection === ARCHIVE_SECTION}
|
||||
onClick={clickHandler(ARCHIVE_SECTION)}>
|
||||
{constants.ARCHIVE}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
</Chip>
|
||||
</Wrapper>
|
||||
{scrollObj.scrollLeft <
|
||||
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||
onClick={scrollCollection(
|
||||
SCROLL_DIRECTION.RIGHT
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</CollectionContainer>
|
||||
<CollectionSort
|
||||
setCollectionSortBy={setCollectionSortBy}
|
||||
activeSortBy={collectionSortBy}
|
||||
/>
|
||||
</CollectionBar>
|
||||
</>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -6,10 +6,10 @@ export enum ButtonVariant {
|
|||
secondary = 'secondary',
|
||||
warning = 'warning',
|
||||
}
|
||||
type Props = React.PropsWithChildren<{
|
||||
onClick: any;
|
||||
export type LinkButtonProps = React.PropsWithChildren<{
|
||||
onClick: () => void;
|
||||
variant?: string;
|
||||
style?: any;
|
||||
style?: React.CSSProperties;
|
||||
}>;
|
||||
|
||||
export function getVariantColor(variant: string) {
|
||||
|
@ -26,7 +26,7 @@ export function getVariantColor(variant: string) {
|
|||
return '#d1d1d1';
|
||||
}
|
||||
}
|
||||
export default function LinkButton(props: Props) {
|
||||
export default function LinkButton(props: LinkButtonProps) {
|
||||
return (
|
||||
<h5
|
||||
style={{
|
||||
|
|
|
@ -8,18 +8,31 @@ import CrossIcon from 'components/icons/CrossIcon';
|
|||
import AddIcon from 'components/icons/AddIcon';
|
||||
import { IconButton } from 'components/Container';
|
||||
import constants from 'utils/strings/constants';
|
||||
import Archive from 'components/icons/Archive';
|
||||
import MoveIcon from 'components/icons/MoveIcon';
|
||||
import { COLLECTION_OPS_TYPE } from 'utils/collection';
|
||||
import { ALL_SECTION, ARCHIVE_SECTION } from './Collections';
|
||||
import UnArchive from 'components/icons/UnArchive';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
import { Collection } from 'services/collectionService';
|
||||
|
||||
interface Props {
|
||||
addToCollectionHelper: (collectionName, collection) => void;
|
||||
moveToCollectionHelper: (collectionName, collection) => void;
|
||||
addToCollectionHelper: (
|
||||
collectionName: string,
|
||||
collection: Collection
|
||||
) => void;
|
||||
moveToCollectionHelper: (
|
||||
collectionName: string,
|
||||
collection: Collection
|
||||
) => void;
|
||||
showCreateCollectionModal: (opsType: COLLECTION_OPS_TYPE) => () => void;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
||||
deleteFileHelper: () => void;
|
||||
count: number;
|
||||
clearSelection: () => void;
|
||||
archiveFilesHelper: () => void;
|
||||
unArchiveFilesHelper: () => void;
|
||||
activeCollection: number;
|
||||
}
|
||||
|
||||
|
@ -29,6 +42,7 @@ const SelectionBar = styled(Navbar)`
|
|||
color: #fff;
|
||||
z-index: 1001;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
`;
|
||||
|
||||
const SelectionContainer = styled.div`
|
||||
|
@ -37,6 +51,14 @@ const SelectionContainer = styled.div`
|
|||
display: flex;
|
||||
`;
|
||||
|
||||
export const IconWithMessage = (props) => (
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
overlay={<p style={{ zIndex: 1002 }}>{props.message}</p>}>
|
||||
{props.children}
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
const SelectedFileOptions = ({
|
||||
addToCollectionHelper,
|
||||
moveToCollectionHelper,
|
||||
|
@ -46,6 +68,8 @@ const SelectedFileOptions = ({
|
|||
deleteFileHelper,
|
||||
count,
|
||||
clearSelection,
|
||||
archiveFilesHelper,
|
||||
unArchiveFilesHelper,
|
||||
activeCollection,
|
||||
}: Props) => {
|
||||
const addToCollection = () =>
|
||||
|
@ -88,17 +112,40 @@ const SelectedFileOptions = ({
|
|||
{count} {constants.SELECTED}
|
||||
</div>
|
||||
</SelectionContainer>
|
||||
{activeCollection !== 0 && (
|
||||
{activeCollection === ARCHIVE_SECTION ? (
|
||||
<IconWithMessage message={constants.UNARCHIVE}>
|
||||
<IconButton onClick={unArchiveFilesHelper}>
|
||||
<UnArchive />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
) : (
|
||||
<>
|
||||
{activeCollection === ALL_SECTION && (
|
||||
<IconWithMessage message={constants.ARCHIVE}>
|
||||
<IconButton onClick={archiveFilesHelper}>
|
||||
<Archive />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
)}
|
||||
{activeCollection !== ALL_SECTION && (
|
||||
<IconWithMessage message={constants.MOVE}>
|
||||
<IconButton onClick={moveToCollection}>
|
||||
<MoveIcon />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
)}
|
||||
<IconWithMessage message={constants.ADD}>
|
||||
<IconButton onClick={addToCollection}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
<IconWithMessage message={constants.DELETE}>
|
||||
<IconButton onClick={deleteHandler}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
</>
|
||||
)}
|
||||
</SelectionBar>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
getLocalFiles,
|
||||
deleteFiles,
|
||||
syncFiles,
|
||||
updateMagicMetadata,
|
||||
VISIBILITY_STATE,
|
||||
} from 'services/fileService';
|
||||
import styled from 'styled-components';
|
||||
import LoadingBar from 'react-top-loading-bar';
|
||||
|
@ -43,7 +45,11 @@ import { useDropzone } from 'react-dropzone';
|
|||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||
import PhotoFrame from 'components/PhotoFrame';
|
||||
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
|
||||
import {
|
||||
changeFilesVisibility,
|
||||
getSelectedFileIds,
|
||||
sortFilesIntoCollections,
|
||||
} from 'utils/file';
|
||||
import SearchBar, { DateValue } from 'components/SearchBar';
|
||||
import { Bbox } from 'services/searchService';
|
||||
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
|
||||
|
@ -106,12 +112,14 @@ type GalleryContextType = {
|
|||
thumbs: Map<number, string>;
|
||||
files: Map<number, string>;
|
||||
showPlanSelectorModal: () => void;
|
||||
setActiveCollection: (collection: number) => void;
|
||||
};
|
||||
|
||||
const defaultGalleryContext: GalleryContextType = {
|
||||
thumbs: new Map(),
|
||||
files: new Map(),
|
||||
showPlanSelectorModal: () => null,
|
||||
setActiveCollection: () => null,
|
||||
};
|
||||
|
||||
export const GalleryContext = createContext<GalleryContextType>(
|
||||
|
@ -318,12 +326,12 @@ export default function Gallery() {
|
|||
setCollectionSelectorView,
|
||||
selected,
|
||||
files,
|
||||
clearSelection,
|
||||
syncWithRemote,
|
||||
|
||||
setActiveCollection,
|
||||
collectionName,
|
||||
collection
|
||||
);
|
||||
clearSelection();
|
||||
} catch (e) {
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
|
@ -331,6 +339,9 @@ export default function Gallery() {
|
|||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
} finally {
|
||||
await syncWithRemote(false, true);
|
||||
loadingBar.current.complete();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -345,12 +356,12 @@ export default function Gallery() {
|
|||
setCollectionSelectorView,
|
||||
selected,
|
||||
files,
|
||||
clearSelection,
|
||||
syncWithRemote,
|
||||
|
||||
setActiveCollection,
|
||||
collectionName,
|
||||
collection
|
||||
);
|
||||
clearSelection();
|
||||
} catch (e) {
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
|
@ -358,6 +369,43 @@ export default function Gallery() {
|
|||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
} finally {
|
||||
await syncWithRemote(false, true);
|
||||
loadingBar.current.complete();
|
||||
}
|
||||
};
|
||||
const changeFilesVisibilityHelper = async (
|
||||
visibility: VISIBILITY_STATE
|
||||
) => {
|
||||
loadingBar.current?.continuousStart();
|
||||
try {
|
||||
const updatedFiles = await changeFilesVisibility(
|
||||
files,
|
||||
selected,
|
||||
visibility
|
||||
);
|
||||
await updateMagicMetadata(updatedFiles);
|
||||
clearSelection();
|
||||
} catch (e) {
|
||||
switch (e.status?.toString()) {
|
||||
case ServerErrorCodes.FORBIDDEN:
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
close: { variant: 'danger' },
|
||||
content: constants.NOT_FILE_OWNER,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
} finally {
|
||||
await syncWithRemote(false, true);
|
||||
loadingBar.current.complete();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -401,10 +449,10 @@ export default function Gallery() {
|
|||
loadingBar.current?.continuousStart();
|
||||
try {
|
||||
const fileIds = getSelectedFileIds(selected);
|
||||
await deleteFiles(fileIds, clearSelection, syncWithRemote);
|
||||
await deleteFiles(fileIds);
|
||||
setDeleted([...deleted, ...fileIds]);
|
||||
clearSelection();
|
||||
} catch (e) {
|
||||
loadingBar.current.complete();
|
||||
switch (e.status?.toString()) {
|
||||
case ServerErrorCodes.FORBIDDEN:
|
||||
setDialogMessage({
|
||||
|
@ -413,8 +461,6 @@ export default function Gallery() {
|
|||
close: { variant: 'danger' },
|
||||
content: constants.NOT_FILE_OWNER,
|
||||
});
|
||||
loadingBar.current.complete();
|
||||
return;
|
||||
}
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
|
@ -422,6 +468,9 @@ export default function Gallery() {
|
|||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
} finally {
|
||||
await syncWithRemote(false, true);
|
||||
loadingBar.current.complete();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -438,7 +487,12 @@ export default function Gallery() {
|
|||
};
|
||||
|
||||
return (
|
||||
<GalleryContext.Provider value={defaultGalleryContext}>
|
||||
<GalleryContext.Provider
|
||||
value={{
|
||||
...defaultGalleryContext,
|
||||
showPlanSelectorModal: () => setPlanModalView(true),
|
||||
setActiveCollection,
|
||||
}}>
|
||||
<FullScreenDropZone
|
||||
getRootProps={getRootProps}
|
||||
getInputProps={getInputProps}>
|
||||
|
@ -478,8 +532,9 @@ export default function Gallery() {
|
|||
/>
|
||||
<Collections
|
||||
collections={collections}
|
||||
collectionAndTheirLatestFile={collectionsAndTheirLatestFile}
|
||||
searchMode={searchMode}
|
||||
selected={activeCollection}
|
||||
activeCollection={activeCollection}
|
||||
setActiveCollection={setActiveCollection}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setDialogMessage={setDialogMessage}
|
||||
|
@ -530,7 +585,6 @@ export default function Gallery() {
|
|||
collections={collections}
|
||||
setDialogMessage={setDialogMessage}
|
||||
setLoading={setLoading}
|
||||
showPlanSelectorModal={() => setPlanModalView(true)}
|
||||
/>
|
||||
<UploadButton
|
||||
isFirstFetch={isFirstFetch}
|
||||
|
@ -558,6 +612,16 @@ export default function Gallery() {
|
|||
selected.collectionID === activeCollection && (
|
||||
<SelectedFileOptions
|
||||
addToCollectionHelper={addToCollectionHelper}
|
||||
archiveFilesHelper={() =>
|
||||
changeFilesVisibilityHelper(
|
||||
VISIBILITY_STATE.ARCHIVED
|
||||
)
|
||||
}
|
||||
unArchiveFilesHelper={() =>
|
||||
changeFilesVisibilityHelper(
|
||||
VISIBILITY_STATE.VISIBLE
|
||||
)
|
||||
}
|
||||
moveToCollectionHelper={moveToCollectionHelper}
|
||||
showCreateCollectionModal={
|
||||
showCreateCollectionModal
|
||||
|
|
|
@ -69,6 +69,12 @@ export interface CollectionAndItsLatestFile {
|
|||
file: File;
|
||||
}
|
||||
|
||||
export enum COLLECTION_SORT_BY {
|
||||
LATEST_FILE,
|
||||
MODIFICATION_TIME,
|
||||
NAME,
|
||||
}
|
||||
|
||||
const getCollectionWithSecrets = async (
|
||||
collection: Collection,
|
||||
masterKey: string
|
||||
|
@ -219,13 +225,9 @@ export const getCollectionsAndTheirLatestFile = (
|
|||
}
|
||||
});
|
||||
const collectionsAndTheirLatestFile: CollectionAndItsLatestFile[] = [];
|
||||
const userID = getData(LS_KEYS.USER)?.id;
|
||||
|
||||
for (const collection of collections) {
|
||||
if (
|
||||
collection.owner.id !== userID ||
|
||||
collection.type === CollectionType.favorites
|
||||
) {
|
||||
if (collection.type === CollectionType.favorites) {
|
||||
continue;
|
||||
}
|
||||
collectionsAndTheirLatestFile.push({
|
||||
|
@ -591,3 +593,62 @@ export const getNonEmptyCollections = (
|
|||
nonEmptyCollectionsIds.has(collection.id)
|
||||
);
|
||||
};
|
||||
|
||||
export function sortCollections(
|
||||
collections: Collection[],
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
|
||||
sortBy: COLLECTION_SORT_BY
|
||||
) {
|
||||
return collections.sort((collectionA, collectionB) => {
|
||||
switch (sortBy) {
|
||||
case COLLECTION_SORT_BY.LATEST_FILE:
|
||||
return compareCollectionsLatestFile(
|
||||
collectionAndTheirLatestFile,
|
||||
collectionA,
|
||||
collectionB
|
||||
);
|
||||
case COLLECTION_SORT_BY.MODIFICATION_TIME:
|
||||
return collectionB.updationTime - collectionA.updationTime;
|
||||
case COLLECTION_SORT_BY.NAME:
|
||||
return collectionA.name.localeCompare(collectionB.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function compareCollectionsLatestFile(
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
|
||||
collectionA: Collection,
|
||||
collectionB: Collection
|
||||
) {
|
||||
const CollectionALatestFile = getCollectionLatestFile(
|
||||
collectionAndTheirLatestFile,
|
||||
collectionA
|
||||
);
|
||||
const CollectionBLatestFile = getCollectionLatestFile(
|
||||
collectionAndTheirLatestFile,
|
||||
collectionB
|
||||
);
|
||||
|
||||
return (
|
||||
CollectionBLatestFile.updationTime - CollectionALatestFile.updationTime
|
||||
);
|
||||
}
|
||||
|
||||
function getCollectionLatestFile(
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
|
||||
collection: Collection
|
||||
) {
|
||||
const collectionAndItsLatestFile = collectionAndTheirLatestFile.filter(
|
||||
(collectionAndItsLatestFile) =>
|
||||
collectionAndItsLatestFile.collection.id === collection.id
|
||||
);
|
||||
if (collectionAndItsLatestFile.length === 1) {
|
||||
return collectionAndItsLatestFile[0].file;
|
||||
} else {
|
||||
logError(
|
||||
Error('collection missing from collectionLatestFile list'),
|
||||
''
|
||||
);
|
||||
return { updationTime: 0 };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,19 @@ export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
|
|||
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm' },
|
||||
];
|
||||
|
||||
export enum VISIBILITY_STATE {
|
||||
VISIBLE,
|
||||
ARCHIVED,
|
||||
}
|
||||
export interface MagicMetadataProps {
|
||||
visibility?: VISIBILITY_STATE;
|
||||
}
|
||||
export interface MagicMetadata {
|
||||
version: number;
|
||||
count: number;
|
||||
data: string | MagicMetadataProps;
|
||||
header: string;
|
||||
}
|
||||
export interface File {
|
||||
id: number;
|
||||
collectionID: number;
|
||||
|
@ -44,6 +57,7 @@ export interface File {
|
|||
file: fileAttribute;
|
||||
thumbnail: fileAttribute;
|
||||
metadata: MetadataObject;
|
||||
magicMetadata: MagicMetadata;
|
||||
encryptedKey: string;
|
||||
keyDecryptionNonce: string;
|
||||
key: string;
|
||||
|
@ -57,6 +71,21 @@ export interface File {
|
|||
updationTime: number;
|
||||
}
|
||||
|
||||
interface UpdateMagicMetadataRequest {
|
||||
metadataList: UpdateMagicMetadata[];
|
||||
}
|
||||
interface UpdateMagicMetadata {
|
||||
id: number;
|
||||
magicMetadata: MagicMetadata;
|
||||
}
|
||||
|
||||
export const NEW_MAGIC_METADATA: MagicMetadata = {
|
||||
version: 0,
|
||||
data: {},
|
||||
header: null,
|
||||
count: 0,
|
||||
};
|
||||
|
||||
export const getLocalFiles = async () => {
|
||||
const files: Array<File> = (await localForage.getItem<File[]>(FILES)) || [];
|
||||
return files;
|
||||
|
@ -203,11 +232,7 @@ const removeDeletedCollectionFiles = async (
|
|||
return files;
|
||||
};
|
||||
|
||||
export const deleteFiles = async (
|
||||
filesToDelete: number[],
|
||||
clearSelection: Function,
|
||||
syncWithRemote: Function
|
||||
) => {
|
||||
export const deleteFiles = async (filesToDelete: number[]) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
|
@ -221,10 +246,25 @@ export const deleteFiles = async (
|
|||
'X-Auth-Token': token,
|
||||
}
|
||||
);
|
||||
clearSelection();
|
||||
syncWithRemote();
|
||||
} catch (e) {
|
||||
logError(e, 'delete failed');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateMagicMetadata = async (files: File[]) => {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
return;
|
||||
}
|
||||
const reqBody: UpdateMagicMetadataRequest = { metadataList: [] };
|
||||
for (const file of files) {
|
||||
reqBody.metadataList.push({
|
||||
id: file.id,
|
||||
magicMetadata: file.magicMetadata,
|
||||
});
|
||||
}
|
||||
await HTTPService.put(`${ENDPOINT}/files/magic-metadata`, reqBody, null, {
|
||||
'X-Auth-Token': token,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -21,8 +21,6 @@ export async function copyOrMoveFromCollection(
|
|||
setCollectionSelectorView: (value: boolean) => void,
|
||||
selected: SelectedState,
|
||||
files: File[],
|
||||
clearSelection: () => void,
|
||||
syncWithRemote: () => Promise<void>,
|
||||
setActiveCollection: (id: number) => void,
|
||||
collectionName: string,
|
||||
existingCollection: Collection
|
||||
|
@ -52,8 +50,6 @@ export async function copyOrMoveFromCollection(
|
|||
default:
|
||||
throw Error(CustomError.INVALID_COLLECTION_OPERATION);
|
||||
}
|
||||
clearSelection();
|
||||
await syncWithRemote();
|
||||
setActiveCollection(collection.id);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,17 @@
|
|||
import { SelectedState } from 'pages/gallery';
|
||||
import { Collection } from 'services/collectionService';
|
||||
import { File, FILE_TYPE } from 'services/fileService';
|
||||
import {
|
||||
File,
|
||||
fileAttribute,
|
||||
FILE_TYPE,
|
||||
MagicMetadataProps,
|
||||
NEW_MAGIC_METADATA,
|
||||
VISIBILITY_STATE,
|
||||
} from 'services/fileService';
|
||||
import { decodeMotionPhoto } from 'services/motionPhotoService';
|
||||
import { getMimeTypeFromBlob } from 'services/upload/readFileService';
|
||||
import { EncryptionResult } from 'services/upload/uploadService';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { User } from 'services/userService';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
|
@ -44,7 +54,7 @@ export function sortFilesIntoCollections(files: File[]) {
|
|||
return collectionWiseFiles;
|
||||
}
|
||||
|
||||
export function getSelectedFileIds(selectedFiles) {
|
||||
export function getSelectedFileIds(selectedFiles: SelectedState) {
|
||||
const filesIDs: number[] = [];
|
||||
for (const [key, val] of Object.entries(selectedFiles)) {
|
||||
if (typeof val === 'boolean' && val) {
|
||||
|
@ -53,7 +63,10 @@ export function getSelectedFileIds(selectedFiles) {
|
|||
}
|
||||
return filesIDs;
|
||||
}
|
||||
export function getSelectedFiles(selectedFiles, files: File[]): File[] {
|
||||
export function getSelectedFiles(
|
||||
selectedFiles: SelectedState,
|
||||
files: File[]
|
||||
): File[] {
|
||||
const filesIDs = new Set(getSelectedFileIds(selectedFiles));
|
||||
const filesToDelete: File[] = [];
|
||||
for (const file of files) {
|
||||
|
@ -124,14 +137,30 @@ export function sortFiles(files: File[]) {
|
|||
}
|
||||
|
||||
export async function decryptFile(file: File, collection: Collection) {
|
||||
try {
|
||||
const worker = await new CryptoWorker();
|
||||
file.key = await worker.decryptB64(
|
||||
file.encryptedKey,
|
||||
file.keyDecryptionNonce,
|
||||
collection.key
|
||||
);
|
||||
file.metadata = await worker.decryptMetadata(file);
|
||||
const encryptedMetadata = file.metadata as unknown as fileAttribute;
|
||||
file.metadata = await worker.decryptMetadata(
|
||||
encryptedMetadata.encryptedData,
|
||||
encryptedMetadata.decryptionHeader,
|
||||
file.key
|
||||
);
|
||||
if (file.magicMetadata?.data) {
|
||||
file.magicMetadata.data = await worker.decryptMetadata(
|
||||
file.magicMetadata.data,
|
||||
file.magicMetadata.header,
|
||||
file.key
|
||||
);
|
||||
}
|
||||
return file;
|
||||
} catch (e) {
|
||||
logError(e, 'file decryption failed');
|
||||
}
|
||||
}
|
||||
|
||||
export function removeUnnecessaryFileProps(files: File[]): File[] {
|
||||
|
@ -188,6 +217,56 @@ export async function convertForPreview(file: File, fileBlob: Blob) {
|
|||
return fileBlob;
|
||||
}
|
||||
|
||||
export function fileIsArchived(file: File) {
|
||||
if (
|
||||
!file ||
|
||||
!file.magicMetadata ||
|
||||
!file.magicMetadata.data ||
|
||||
typeof file.magicMetadata.data === 'string' ||
|
||||
typeof file.magicMetadata.data.visibility === 'undefined'
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return file.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED;
|
||||
}
|
||||
|
||||
export async function changeFilesVisibility(
|
||||
files: File[],
|
||||
selected: SelectedState,
|
||||
visibility: VISIBILITY_STATE
|
||||
) {
|
||||
const worker = await new CryptoWorker();
|
||||
const selectedFiles = getSelectedFiles(selected, files);
|
||||
const updatedFiles: File[] = [];
|
||||
for (const file of selectedFiles) {
|
||||
if (!file.magicMetadata) {
|
||||
file.magicMetadata = NEW_MAGIC_METADATA;
|
||||
}
|
||||
if (typeof file.magicMetadata.data === 'string') {
|
||||
logError(Error('magic metadata not decrypted'), '');
|
||||
return;
|
||||
}
|
||||
// copies the existing magic metadata properties of the files and updates the visibility value
|
||||
// The expected behaviour while updating magic metadata is to let the existing property as it is and update/add the property you want
|
||||
const updatedMagicMetadataProps: MagicMetadataProps = {
|
||||
...file.magicMetadata.data,
|
||||
visibility,
|
||||
};
|
||||
const encryptedMagicMetadata: EncryptionResult =
|
||||
await worker.encryptMetadata(updatedMagicMetadataProps, file.key);
|
||||
updatedFiles.push({
|
||||
...file,
|
||||
magicMetadata: {
|
||||
version: file.magicMetadata.version,
|
||||
count: Object.keys(updatedMagicMetadataProps).length,
|
||||
data: encryptedMagicMetadata.file
|
||||
.encryptedData as unknown as string,
|
||||
header: encryptedMagicMetadata.file.decryptionHeader,
|
||||
},
|
||||
});
|
||||
}
|
||||
return updatedFiles;
|
||||
}
|
||||
export function isSharedFile(file: File) {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
|
|
|
@ -420,7 +420,7 @@ const englishConstants = {
|
|||
</span>
|
||||
),
|
||||
NOT_FILE_OWNER: 'you cannot delete files in a shared album',
|
||||
ADD_TO_COLLECTION: 'add to collection',
|
||||
ADD_TO_COLLECTION: 'add to album',
|
||||
SELECTED: 'selected',
|
||||
VIDEO_PLAYBACK_FAILED: 'video format not supported',
|
||||
VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD:
|
||||
|
@ -542,7 +542,16 @@ const englishConstants = {
|
|||
TOO_LARGE_INFO:
|
||||
'these files were not uploaded as they exceed the maximum size limit for your storage plan',
|
||||
UPLOAD_TO_COLLECTION: 'upload to album',
|
||||
MOVE_TO_COLLECTION: 'move to collection',
|
||||
ARCHIVE: 'archive',
|
||||
ALL: 'all',
|
||||
MOVE_TO_COLLECTION: 'move to album',
|
||||
UNARCHIVE: 'un-archive',
|
||||
MOVE: 'move',
|
||||
ADD: 'add',
|
||||
SORT: 'sort',
|
||||
SORT_BY_LATEST_PHOTO: 'most recent photo',
|
||||
SORT_BY_MODIFICATION_TIME: 'last modified',
|
||||
SORT_BY_COLLECTION_NAME: 'album title',
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
|
@ -3,11 +3,11 @@ import * as libsodium from 'utils/crypto/libsodium';
|
|||
import { convertHEIC2JPEG } from 'utils/file/convertHEIC';
|
||||
|
||||
export class Crypto {
|
||||
async decryptMetadata(file) {
|
||||
async decryptMetadata(encryptedMetadata, header, key) {
|
||||
const encodedMetadata = await libsodium.decryptChaChaOneShot(
|
||||
await libsodium.fromB64(file.metadata.encryptedData),
|
||||
await libsodium.fromB64(file.metadata.decryptionHeader),
|
||||
file.key
|
||||
await libsodium.fromB64(encryptedMetadata),
|
||||
await libsodium.fromB64(header),
|
||||
key
|
||||
);
|
||||
return JSON.parse(new TextDecoder().decode(encodedMetadata));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue