Merge pull request #161 from ente-io/sort-collections

Sort collections
This commit is contained in:
Abhinav-grd 2021-09-28 10:32:46 +05:30 committed by GitHub
commit 20efc76a8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 367 additions and 117 deletions

View file

@ -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;

View 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',
};

View 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',
};

View file

@ -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>

View file

@ -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)
);
if (collectionsOtherThanFrom.length === 0) {
const user: User = getData(LS_KEYS.USER);
const personalCollectionsOtherThanFrom =
collectionsAndTheirLatestFile?.filter(
(item) =>
item.collection.id !== attributes.fromCollection &&
item.collection.owner.id === user?.id
);
if (personalCollectionsOtherThanFrom.length === 0) {
props.onHide();
attributes.showNextModal();
} else {
setCollectionToShow(collectionsOtherThanFrom);
setCollectionToShow(personalCollectionsOtherThanFrom);
}
}, [props.show]);

View 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>
);
}

View 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;

View file

@ -5,7 +5,13 @@ 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';
@ -14,6 +20,7 @@ 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;
@ -21,6 +28,7 @@ export const ALL_SECTION = 0;
interface CollectionProps {
collections: Collection[];
collectionAndTheirLatestFile: CollectionAndItsLatestFile[];
activeCollection?: number;
setActiveCollection: (id?: number) => void;
setDialogMessage: SetDialogMessage;
@ -31,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;
@ -54,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;
@ -84,6 +99,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 (collectionRef.current) {
@ -165,81 +182,96 @@ export default function Collections(props: CollectionProps) {
)}
syncWithRemote={props.syncWithRemote}
/>
<Container>
{scrollObj.scrollLeft > 0 && (
<NavigationButton
scrollDirection={SCROLL_DIRECTION.LEFT}
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
/>
)}
<Wrapper ref={collectionRef} onScroll={updateScrollObj}>
<Chip
active={activeCollection === ALL_SECTION}
onClick={clickHandler(ALL_SECTION)}>
{constants.ALL}
<div
style={{
display: 'inline-block',
width: '24px',
}}
<CollectionBar>
<CollectionContainer>
{scrollObj.scrollLeft > 0 && (
<NavigationButton
scrollDirection={SCROLL_DIRECTION.LEFT}
onClick={scrollCollection(
SCROLL_DIRECTION.LEFT
)}
/>
</Chip>
{collections?.map((item) => (
<OverlayTrigger
key={item.id}
placement="top"
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip(item.id)}>
<Chip
active={activeCollection === item.id}
onClick={clickHandler(item.id)}>
{item.name}
{item.type !== CollectionType.favorites &&
item.owner.id === user?.id ? (
<OverlayTrigger
rootClose
trigger="click"
placement="bottom"
overlay={collectionOptions}>
<OptionIcon
onClick={() =>
setSelectedCollectionID(
item.id
)
}
)}
<Wrapper ref={collectionRef} onScroll={updateScrollObj}>
<Chip
active={activeCollection === ALL_SECTION}
onClick={clickHandler(ALL_SECTION)}>
{constants.ALL}
<div
style={{
display: 'inline-block',
width: '24px',
}}
/>
</Chip>
{sortCollections(
collections,
props.collectionAndTheirLatestFile,
collectionSortBy
).map((item) => (
<OverlayTrigger
key={item.id}
placement="top"
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip(item.id)}>
<Chip
active={activeCollection === item.id}
onClick={clickHandler(item.id)}>
{item.name}
{item.type !==
CollectionType.favorites &&
item.owner.id === user?.id ? (
<OverlayTrigger
rootClose
trigger="click"
placement="bottom"
overlay={collectionOptions}>
<OptionIcon
onClick={() =>
setSelectedCollectionID(
item.id
)
}
/>
</OverlayTrigger>
) : (
<div
style={{
display: 'inline-block',
width: '24px',
}}
/>
</OverlayTrigger>
) : (
<div
style={{
display: 'inline-block',
width: '24px',
}}
/>
)}
</Chip>
</OverlayTrigger>
))}
<Chip
active={activeCollection === ARCHIVE_SECTION}
onClick={clickHandler(ARCHIVE_SECTION)}>
{constants.ARCHIVE}
<div
style={{
display: 'inline-block',
width: '24px',
}}
)}
</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
)}
/>
</Chip>
</Wrapper>
{scrollObj.scrollLeft <
scrollObj.scrollWidth - scrollObj.clientWidth && (
<NavigationButton
scrollDirection={SCROLL_DIRECTION.RIGHT}
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
/>
)}
</Container>
)}
</CollectionContainer>
<CollectionSort
setCollectionSortBy={setCollectionSortBy}
activeSortBy={collectionSortBy}
/>
</CollectionBar>
</>
)
);

View file

@ -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={{

View file

@ -51,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,
@ -94,14 +102,6 @@ const SelectedFileOptions = ({
});
};
const IconWithMessage = (props) => (
<OverlayTrigger
placement="bottom"
overlay={<p style={{ zIndex: 1002 }}>{props.message}</p>}>
{props.children}
</OverlayTrigger>
);
return (
<SelectionBar>
<SelectionContainer>

View file

@ -526,6 +526,7 @@ export default function Gallery() {
/>
<Collections
collections={collections}
collectionAndTheirLatestFile={collectionsAndTheirLatestFile}
searchMode={searchMode}
activeCollection={activeCollection}
setActiveCollection={setActiveCollection}

View file

@ -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 };
}
}

View file

@ -548,6 +548,10 @@ const englishConstants = {
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;