Merge pull request #594 from ente-io/collection-title-scroll

Collection title scroll
This commit is contained in:
Abhinav Kumar 2022-06-13 10:47:06 +05:30 committed by GitHub
commit 72a431e22b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 151 additions and 119 deletions

View file

@ -5,6 +5,7 @@ import { SCROLL_DIRECTION } from 'hooks/useComponentScroll';
const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
position: absolute;
z-index: 2;
top: 7px;
height: 50px;
width: 50px;

View file

@ -5,10 +5,9 @@ import constants from 'utils/strings/constants';
import { ALL_SECTION, COLLECTION_SORT_BY } from 'constants/collection';
import { Typography } from '@mui/material';
import {
Hider,
CollectionBarWrapper,
CollectionListBarWrapper,
ScrollContainer,
CollectionSectionWrapper,
CollectionListWrapper,
} from 'components/Collections/styledComponents';
import CollectionCardWithActiveIndicator from 'components/Collections/CollectionBar/CollectionCardWithActiveIndicator';
import useComponentScroll, { SCROLL_DIRECTION } from 'hooks/useComponentScroll';
@ -20,15 +19,13 @@ import { sortCollectionSummaries } from 'services/collectionService';
interface IProps {
activeCollection?: number;
setActiveCollection: (id?: number) => void;
isInSearchMode: boolean;
collectionSummaries: CollectionSummaries;
showAllCollections: () => void;
}
export default function CollectionBar(props: IProps) {
export default function CollectionListBar(props: IProps) {
const {
activeCollection,
setActiveCollection,
collectionSummaries,
showAllCollections,
@ -76,21 +73,17 @@ export default function CollectionBar(props: IProps) {
};
return (
<Hider hide={props.isInSearchMode}>
<CollectionSectionWrapper>
<SpaceBetweenFlex>
<Typography fontWeight={'bold'}>
{constants.ALBUMS}
</Typography>
{hasScrollBar && (
<LinkButton onClick={showAllCollections}>
{constants.VIEW_ALL_ALBUMS}
</LinkButton>
)}
</SpaceBetweenFlex>
</CollectionSectionWrapper>
<CollectionListBarWrapper>
<SpaceBetweenFlex mb={1}>
<Typography>{constants.ALBUMS}</Typography>
{hasScrollBar && (
<LinkButton onClick={showAllCollections}>
{constants.VIEW_ALL_ALBUMS}
</LinkButton>
)}
</SpaceBetweenFlex>
<CollectionBarWrapper>
<CollectionListWrapper>
{!onFarLeft && (
<ScrollButton
scrollDirection={SCROLL_DIRECTION.LEFT}
@ -115,7 +108,7 @@ export default function CollectionBar(props: IProps) {
onClick={scrollComponent(SCROLL_DIRECTION.RIGHT)}
/>
)}
</CollectionBarWrapper>
</Hider>
</CollectionListWrapper>
</CollectionListBarWrapper>
);
}

View file

@ -1,19 +1,15 @@
import { CollectionInfo } from './CollectionInfo';
import React from 'react';
import { Collection, CollectionSummary } from 'types/collection';
import {
CollectionSectionWrapper,
Hider,
} from 'components/Collections/styledComponents';
import CollectionOptions from 'components/Collections/CollectionOptions';
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
import { SPECIAL_COLLECTION_TYPES } from 'constants/collection';
import { SpaceBetweenFlex } from 'components/Container';
import { CollectionInfoBarWrapper } from './styledComponents';
interface Iprops {
activeCollection: Collection;
collectionSummary: CollectionSummary;
isInSearchMode: boolean;
setCollectionNamerAttributes: SetCollectionNamerAttributes;
showCollectionShareModal: () => void;
redirectToAll: () => void;
@ -37,15 +33,13 @@ export default function CollectionInfoWithOptions({
const { name, type, fileCount } = collectionSummary;
return (
<Hider hide={props.isInSearchMode}>
<CollectionSectionWrapper>
<SpaceBetweenFlex>
<CollectionInfo name={name} fileCount={fileCount} />
{!SPECIAL_COLLECTION_TYPES.has(type) && (
<CollectionOptions {...props} />
)}
</SpaceBetweenFlex>
</CollectionSectionWrapper>
</Hider>
<CollectionInfoBarWrapper>
<SpaceBetweenFlex>
<CollectionInfo name={name} fileCount={fileCount} />
{!SPECIAL_COLLECTION_TYPES.has(type) && (
<CollectionOptions {...props} />
)}
</SpaceBetweenFlex>
</CollectionInfoBarWrapper>
);
}

View file

@ -1,11 +1,13 @@
import { Collection, CollectionSummaries } from 'types/collection';
import CollectionBar from 'components/Collections/CollectionBar';
import CollectionListBar from 'components/Collections/CollectionBar';
import React, { useEffect, useRef, useState } from 'react';
import AllCollections from 'components/Collections/AllCollections';
import CollectionInfoWithOptions from 'components/Collections/CollectionInfoWithOptions';
import { ALL_SECTION } from 'constants/collection';
import CollectionShare from 'components/Collections/CollectionShare';
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
interface Iprops {
collections: Collection[];
activeCollectionID?: number;
@ -13,6 +15,7 @@ interface Iprops {
isInSearchMode: boolean;
collectionSummaries: CollectionSummaries;
setCollectionNamerAttributes: SetCollectionNamerAttributes;
setPhotoListHeader: (value: TimeStampListItem) => void;
}
export default function Collections(props: Iprops) {
@ -23,6 +26,7 @@ export default function Collections(props: Iprops) {
setActiveCollectionID,
collectionSummaries,
setCollectionNamerAttributes,
setPhotoListHeader,
} = props;
const [allCollectionView, setAllCollectionView] = useState(false);
@ -31,6 +35,8 @@ export default function Collections(props: Iprops) {
const collectionsMap = useRef<Map<number, Collection>>(new Map());
const activeCollection = useRef<Collection>(null);
const shouldBeHidden = isInSearchMode || collectionSummaries?.size <= 3;
useEffect(() => {
collectionsMap.current = new Map(
props.collections.map((collection) => [collection.id, collection])
@ -42,10 +48,38 @@ export default function Collections(props: Iprops) {
collectionsMap.current.get(activeCollectionID);
}, [activeCollectionID, collections]);
useEffect(
() =>
!shouldBeHidden &&
setPhotoListHeader({
item: (
<CollectionInfoWithOptions
collectionSummary={collectionSummaries.get(
activeCollectionID
)}
activeCollection={activeCollection.current}
setCollectionNamerAttributes={
setCollectionNamerAttributes
}
redirectToAll={() => setActiveCollectionID(ALL_SECTION)}
showCollectionShareModal={() =>
setCollectionShareModalView(true)
}
/>
),
itemType: ITEM_TYPE.STATIC,
height: 80,
}),
[collectionSummaries, activeCollectionID, shouldBeHidden]
);
if (shouldBeHidden) {
return <></>;
}
return (
<>
<CollectionBar
isInSearchMode={isInSearchMode}
<CollectionListBar
activeCollection={activeCollectionID}
setActiveCollection={setActiveCollectionID}
collectionSummaries={collectionSummaries}
@ -59,16 +93,6 @@ export default function Collections(props: Iprops) {
setActiveCollection={setActiveCollectionID}
/>
<CollectionInfoWithOptions
isInSearchMode={isInSearchMode}
collectionSummary={collectionSummaries.get(activeCollectionID)}
activeCollection={activeCollection.current}
setCollectionNamerAttributes={setCollectionNamerAttributes}
redirectToAll={() => setActiveCollectionID(ALL_SECTION)}
showCollectionShareModal={() =>
setCollectionShareModalView(true)
}
/>
<CollectionShare
show={collectionShareModalView}
onHide={() => setCollectionShareModalView(false)}

View file

@ -1,26 +1,28 @@
import { Box } from '@mui/material';
import { PaddedContainer } from 'components/Container';
import styled from 'styled-components';
export const CollectionBarWrapper = styled(PaddedContainer)`
display: flex;
export const CollectionListWrapper = styled(Box)`
position: relative;
overflow: hidden;
height: 86px;
width: 100%;
margin: 10px auto;
`;
export const CollectionListBarWrapper = styled(PaddedContainer)`
width: 100%;
margin: 16px auto;
border-bottom: 1px solid ${({ theme }) => theme.palette.divider};
`;
export const CollectionSectionWrapper = styled(PaddedContainer)`
margin-bottom: 8px;
margin-top: 16px;
export const CollectionInfoBarWrapper = styled(Box)`
margin-bottom: 24px;
`;
export const ScrollContainer = styled.div`
width: 100%;
height: 100px;
overflow: auto;
max-width: 100%;
scroll-behavior: smooth;
display: flex;
`;

View file

@ -17,20 +17,22 @@ import { ENTE_WEBSITE_LINK } from 'constants/urls';
import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton';
import { convertBytesToHumanReadable } from 'utils/billing';
import { DeduplicateContext } from 'pages/deduplicate';
import { PaddedContainer } from './Container';
import { FlexWrapper, PaddedContainer } from './Container';
import { Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery';
const A_DAY = 24 * 60 * 60 * 1000;
const NO_OF_PAGES = 2;
const FOOTER_HEIGHT = 90;
enum ITEM_TYPE {
export enum ITEM_TYPE {
TIME = 'TIME',
TILE = 'TILE',
FILE = 'FILE',
SIZE_AND_COUNT = 'SIZE_AND_COUNT',
OTHER = 'OTHER',
STATIC = 'static',
}
interface TimeStampListItem {
export interface TimeStampListItem {
itemType: ITEM_TYPE;
items?: EnteFile[];
itemStartIndex?: number;
@ -79,56 +81,40 @@ const ListContainer = styled(PaddedContainer)<{
color: #fff;
`;
const DateContainer = styled.div<{ span: number }>`
const ListItemContainer = styled(FlexWrapper)<{ span: number }>`
grid-column: span ${(props) => props.span};
user-select: none;
`;
const DateContainer = styled(ListItemContainer)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
grid-column: span ${(props) => props.span};
display: flex;
align-items: center;
height: ${DATE_CONTAINER_HEIGHT}px;
`;
const SizeAndCountContainer = styled.div<{ span: number }>`
user-select: none;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
grid-column: span ${(props) => props.span};
display: flex;
align-items: center;
const SizeAndCountContainer = styled(DateContainer)`
margin-top: 1rem;
height: ${SIZE_AND_COUNT_CONTAINER_HEIGHT}px;
`;
const FooterContainer = styled.div<{ span: number }>`
const FooterContainer = styled(ListItemContainer)`
font-size: 14px;
margin-bottom: 0.75rem;
@media (max-width: 540px) {
font-size: 12px;
margin-bottom: 0.5rem;
}
color: #979797;
text-align: center;
grid-column: span ${(props) => props.span};
display: flex;
justify-content: center;
align-items: flex-end;
& > p {
margin: 0;
}
margin-top: calc(2rem + 20px);
`;
const NothingContainer = styled.div<{ span: number }>`
const NothingContainer = styled(ListItemContainer)`
color: #979797;
text-align: center;
grid-column: span ${(props) => props.span};
display: flex;
justify-content: center;
align-items: center;
`;
interface Props {
@ -150,6 +136,7 @@ export function PhotoList({
activeCollection,
resetFetching,
}: Props) {
const galleryContext = useContext(GalleryContext);
const timeStampListRef = useRef([]);
const timeStampList = timeStampListRef?.current ?? [];
const filteredDataCopyRef = useRef([]);
@ -176,7 +163,7 @@ export function PhotoList({
};
useEffect(() => {
let timeStampList: TimeStampListItem[] = [];
let timeStampList: TimeStampListItem[] = [getPhotoListHeader()];
if (deduplicateContext.isOnDeduplicatePage) {
skipMerge = true;
groupByFileSize(timeStampList);
@ -244,7 +231,7 @@ export function PhotoList({
while (index <= lastFileIndex) {
const tileSize = Math.min(columns, lastFileIndex - index + 1);
timeStampList.push({
itemType: ITEM_TYPE.TILE,
itemType: ITEM_TYPE.FILE,
items: filteredData.slice(index, index + tileSize),
itemStartIndex: index,
});
@ -284,7 +271,7 @@ export function PhotoList({
id: currentDate.toString(),
});
timeStampList.push({
itemType: ITEM_TYPE.TILE,
itemType: ITEM_TYPE.FILE,
items: [item],
itemStartIndex: index,
});
@ -295,7 +282,7 @@ export function PhotoList({
} else {
listItemIndex = 1;
timeStampList.push({
itemType: ITEM_TYPE.TILE,
itemType: ITEM_TYPE.FILE,
items: [item],
itemStartIndex: index,
});
@ -308,9 +295,20 @@ export function PhotoList({
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
const getPhotoListHeader = () => {
return {
...galleryContext.photoListHeader,
item: (
<ListItemContainer span={columns}>
{galleryContext.photoListHeader.item}
</ListItemContainer>
),
};
};
const getEmptyListItem = () => {
return {
itemType: ITEM_TYPE.OTHER,
itemType: ITEM_TYPE.STATIC,
item: (
<NothingContainer span={columns}>
<div>{constants.NOTHING_HERE}</div>
@ -333,18 +331,19 @@ export function PhotoList({
return sum;
})();
return {
itemType: ITEM_TYPE.OTHER,
itemType: ITEM_TYPE.STATIC,
item: <></>,
height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0),
};
};
const getAppDownloadFooter = () => {
return {
itemType: ITEM_TYPE.OTHER,
itemType: ITEM_TYPE.STATIC,
height: FOOTER_HEIGHT,
item: (
<FooterContainer span={columns}>
<p>{constants.INSTALL_MOBILE_APP()}</p>
<Typography>{constants.INSTALL_MOBILE_APP()}</Typography>
</FooterContainer>
),
};
@ -352,7 +351,7 @@ export function PhotoList({
const getAlbumsFooter = () => {
return {
itemType: ITEM_TYPE.OTHER,
itemType: ITEM_TYPE.STATIC,
height: FOOTER_HEIGHT,
item: (
<FooterContainer span={columns}>
@ -456,7 +455,7 @@ export function PhotoList({
return DATE_CONTAINER_HEIGHT;
case ITEM_TYPE.SIZE_AND_COUNT:
return SIZE_AND_COUNT_CONTAINER_HEIGHT;
case ITEM_TYPE.TILE:
case ITEM_TYPE.FILE:
return listItemHeight;
default:
return timeStampList[index].height;
@ -469,7 +468,7 @@ export function PhotoList({
const generateKey = (index) => {
switch (timeStampList[index].itemType) {
case ITEM_TYPE.TILE:
case ITEM_TYPE.FILE:
return `${timeStampList[index].items[0].id}-${
timeStampList[index].items.slice(-1)[0].id
}`;
@ -503,9 +502,7 @@ export function PhotoList({
{constants.EACH}
</SizeAndCountContainer>
);
case ITEM_TYPE.OTHER:
return listItem.item;
default: {
case ITEM_TYPE.FILE: {
const ret = listItem.items.map((item, idx) =>
getThumbnail(
filteredDataCopy,
@ -522,6 +519,8 @@ export function PhotoList({
}
return ret;
}
default:
return listItem.item;
}
};
if (!timeStampList?.length) {

View file

@ -2,8 +2,8 @@ import React from 'react';
import { CollectionInfo } from 'components/Collections/CollectionInfo';
import constants from 'utils/strings/constants';
import { Typography } from '@mui/material';
import { CollectionSectionWrapper } from 'components/Collections/styledComponents';
import { SearchResultSummary } from 'types/search';
import { CollectionInfoBarWrapper } from 'components/Collections/styledComponents';
interface Iprops {
searchResultSummary: SearchResultSummary;
@ -17,11 +17,11 @@ export default function SearchResultInfo({ searchResultSummary }: Iprops) {
console.log(optionName, fileCount);
return (
<CollectionSectionWrapper>
<CollectionInfoBarWrapper>
<Typography variant="subtitle" color="text.secondary">
{constants.SEARCH_RESULTS}
</Typography>
<CollectionInfo name={optionName} fileCount={fileCount} />
</CollectionSectionWrapper>
</CollectionInfoBarWrapper>
);
}

View file

@ -102,6 +102,7 @@ import { GalleryNavbar } from 'components/pages/gallery/Navbar';
import { Search, SearchResultSummary } from 'types/search';
import SearchResultInfo from 'components/Search/SearchResultInfo';
import { NotificationAttributes } from 'types/Notification';
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
export const DeadCenter = styled.div`
flex: 1;
@ -126,6 +127,7 @@ const defaultGalleryContext: GalleryContextType = {
syncWithRemote: () => null,
setNotificationAttributes: () => null,
setBlockingLoad: () => null,
photoListHeader: null,
};
export const GalleryContext = createContext<GalleryContextType>(
@ -134,7 +136,7 @@ export const GalleryContext = createContext<GalleryContextType>(
export default function Gallery() {
const router = useRouter();
const [collections, setCollections] = useState<Collection[]>([]);
const [collections, setCollections] = useState<Collection[]>(null);
const [files, setFiles] = useState<EnteFile[]>(null);
const [favItemIds, setFavItemIds] = useState<Set<number>>();
@ -176,7 +178,7 @@ export default function Gallery() {
const { startLoading, finishLoading, setDialogMessage, ...appContext } =
useContext(AppContext);
const [collectionSummaries, setCollectionSummaries] =
useState<CollectionSummaries>(new Map());
useState<CollectionSummaries>();
const [activeCollection, setActiveCollection] = useState<number>(undefined);
const [trash, setTrash] = useState<Trash>([]);
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
@ -203,6 +205,8 @@ export default function Gallery() {
const closeSidebar = () => setSidebarView(false);
const openSidebar = () => setSidebarView(true);
const [droppedFiles, setDroppedFiles] = useState([]);
const [photoListHeader, setPhotoListHeader] =
useState<TimeStampListItem>(null);
const showSessionExpiredMessage = () =>
setDialogMessage({
@ -240,7 +244,6 @@ export default function Gallery() {
setFiles(sortFiles(files));
setCollections(collections);
setTrash(trash);
await setDerivativeState(collections, files);
await syncWithRemote(true);
setIsFirstLoad(false);
setJustSignedUp(false);
@ -249,6 +252,10 @@ export default function Gallery() {
main();
}, []);
useEffect(() => {
setDerivativeState(collections, files);
}, [collections, files]);
useEffect(
() => collectionSelectorAttributes && setCollectionSelectorView(true),
[collectionSelectorAttributes]
@ -300,6 +307,20 @@ export default function Gallery() {
}
}, [router.isReady]);
useEffect(() => {
if (isInSearchMode) {
setPhotoListHeader({
height: 116,
item: (
<SearchResultInfo
searchResultSummary={searchResultSummary}
/>
),
itemType: ITEM_TYPE.STATIC,
});
}
}, [isInSearchMode, searchResultSummary]);
const syncWithRemote = async (force = false, silent = false) => {
if (syncInProgress.current && !force) {
resync.current = true;
@ -319,7 +340,6 @@ export default function Gallery() {
const trash = await syncTrash(collections, setFiles, files);
setTrash(trash);
files.push(...getTrashedFiles(trash));
await setDerivativeState(collections, files);
} catch (e) {
logError(e, 'syncWithRemote failed');
switch (e.message) {
@ -345,17 +365,17 @@ export default function Gallery() {
collections: Collection[],
files: EnteFile[]
) => {
if (!collections || !files) {
return;
}
const favItemIds = await getFavItemIds(files);
setFavItemIds(favItemIds);
const nonEmptyCollections = getNonEmptyCollections(collections, files);
setCollections(nonEmptyCollections);
const collectionSummaries = getCollectionSummaries(
nonEmptyCollections,
files
);
setCollectionSummaries(collectionSummaries);
const archivedCollections = getArchivedCollections(nonEmptyCollections);
@ -366,7 +386,7 @@ export default function Gallery() {
setSelected({ count: 0, collectionID: 0 });
};
if (!files) {
if (!files || !collectionSummaries) {
return <div />;
}
const collectionOpsHelper =
@ -578,6 +598,7 @@ export default function Gallery() {
syncWithRemote,
setNotificationAttributes,
setBlockingLoad,
photoListHeader: photoListHeader,
}}>
<FullScreenDropZone
getRootProps={getRootProps}
@ -641,12 +662,8 @@ export default function Gallery() {
setActiveCollectionID={setActiveCollection}
collectionSummaries={collectionSummaries}
setCollectionNamerAttributes={setCollectionNamerAttributes}
setPhotoListHeader={setPhotoListHeader}
/>
{isInSearchMode && (
<SearchResultInfo
searchResultSummary={searchResultSummary}
/>
)}
<Upload
syncWithRemote={syncWithRemote}

View file

@ -34,7 +34,7 @@ import { Card } from 'react-bootstrap';
import { logError } from 'utils/sentry';
import SharedAlbumNavbar from 'components/pages/sharedAlbum/Navbar';
import { CollectionInfo } from 'components/Collections/CollectionInfo';
import { CollectionSectionWrapper } from 'components/Collections/styledComponents';
import { CollectionInfoBarWrapper } from 'components/Collections/styledComponents';
const Loader = () => (
<VerticallyCentered>
@ -280,12 +280,12 @@ export default function PublicCollectionGallery() {
openReportForm,
}}>
<SharedAlbumNavbar />
<CollectionSectionWrapper>
<CollectionInfoBarWrapper>
<CollectionInfo
name={publicCollection.name}
fileCount={publicFiles.length}
/>
</CollectionSectionWrapper>
</CollectionInfoBarWrapper>
<PhotoFrame
files={publicFiles}
setFiles={setPublicFiles}

View file

@ -1,3 +1,4 @@
import { TimeStampListItem } from 'components/PhotoList';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
import { NotificationAttributes } from 'types/Notification';
@ -24,4 +25,5 @@ export type GalleryContextType = {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
setNotificationAttributes: (attributes: NotificationAttributes) => void;
setBlockingLoad: (value: boolean) => void;
photoListHeader: TimeStampListItem;
};