Merge branch 'master' into release

This commit is contained in:
Abhinav-grd 2021-05-26 21:08:23 +05:30
commit c3f284cc0e
23 changed files with 1069 additions and 1715 deletions

View file

@ -17,7 +17,9 @@
"@stripe/stripe-js": "^1.13.2", "@stripe/stripe-js": "^1.13.2",
"axios": "^0.20.0", "axios": "^0.20.0",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
"chrono-node": "^2.2.6",
"comlink": "^4.3.0", "comlink": "^4.3.0",
"debounce-promise": "^3.1.2",
"exif-js": "^2.3.0", "exif-js": "^2.3.0",
"formik": "^2.1.5", "formik": "^2.1.5",
"heic2any": "^0.0.3", "heic2any": "^0.0.3",
@ -26,6 +28,7 @@
"libsodium-wrappers": "^0.7.8", "libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"next": "9.5.3", "next": "9.5.3",
"next-on-netlify": "^3.0.1",
"next-with-workbox": "^2.0.1", "next-with-workbox": "^2.0.1",
"node-forge": "^0.10.0", "node-forge": "^0.10.0",
"photoswipe": "file:./thirdparty/photoswipe", "photoswipe": "file:./thirdparty/photoswipe",
@ -34,6 +37,7 @@
"react-burger-menu": "^3.0.4", "react-burger-menu": "^3.0.4",
"react-dom": "16.13.1", "react-dom": "16.13.1",
"react-dropzone": "^11.2.4", "react-dropzone": "^11.2.4",
"react-select": "^4.3.1",
"react-top-loading-bar": "^2.0.1", "react-top-loading-bar": "^2.0.1",
"react-virtualized-auto-sizer": "^1.0.2", "react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6", "react-window": "^1.8.6",
@ -50,17 +54,18 @@
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^9.5.3", "@next/bundle-analyzer": "^9.5.3",
"@types/debounce-promise": "^3.1.3",
"@types/libsodium-wrappers": "^0.7.8", "@types/libsodium-wrappers": "^0.7.8",
"@types/localforage": "^0.0.34", "@types/localforage": "^0.0.34",
"@types/node": "^14.6.4", "@types/node": "^14.6.4",
"@types/photoswipe": "^4.1.1", "@types/photoswipe": "^4.1.1",
"@types/react": "^16.9.49", "@types/react": "^16.9.49",
"@types/react-select": "^4.0.15",
"@types/react-window": "^1.8.2", "@types/react-window": "^1.8.2",
"@types/react-window-infinite-loader": "^1.0.3", "@types/react-window-infinite-loader": "^1.0.3",
"@types/styled-components": "^5.1.3", "@types/styled-components": "^5.1.3",
"@types/yup": "^0.29.7", "@types/yup": "^0.29.7",
"babel-plugin-styled-components": "^1.11.1", "babel-plugin-styled-components": "^1.11.1",
"next-on-netlify": "^2.4.0",
"typescript": "^4.1.3", "typescript": "^4.1.3",
"worker-plugin": "^5.0.0" "worker-plugin": "^5.0.0"
}, },

BIN
public/images/ente-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/images/ente-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
public/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

View file

@ -6,6 +6,16 @@
"src": "/images/ente-192.png", "src": "/images/ente-192.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
},
{
"src": "/images/ente-256.png",
"type": "image/png",
"sizes": "256x256"
},
{
"src": "/images/ente-512.png",
"type": "image/png",
"sizes": "512x512"
} }
], ],
"start_url": "/", "start_url": "/",

View file

@ -0,0 +1,20 @@
import React from 'react';
export default function DateIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path>
</svg>
);
}
DateIcon.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -0,0 +1,20 @@
import React from 'react';
export default function DateIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
<path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm-4.5-7a2.5 2.5 0 0 0 0 5 2.5 2.5 0 0 0 0-5z"></path>
</svg>
);
}
DateIcon.defaultProps = {
height: 20,
width: 20,
viewBox: '0 0 24 24',
};

View file

@ -0,0 +1,22 @@
import React from 'react';
import styled from 'styled-components';
export default function LocationIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zM7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9z"></path>
<circle cx="12" cy="9" r="2.5"></circle>
</svg>
);
}
LocationIcon.defaultProps = {
height: 20,
width: 20,
viewBox: '0 0 24 24',
};

View file

@ -80,7 +80,7 @@ const EmptyScreen = styled.div`
color: #2dc262; color: #2dc262;
& > svg { & > svg {
filter: drop-shadow(3px 3px 5px rgba(45,194,98,0.5)); filter: drop-shadow(3px 3px 5px rgba(45, 194, 98, 0.5));
} }
`; `;
@ -101,6 +101,7 @@ interface Props {
isFirstLoad; isFirstLoad;
openFileUploader; openFileUploader;
loadingBar; loadingBar;
searchMode: boolean;
} }
const PhotoFrame = ({ const PhotoFrame = ({
@ -114,6 +115,7 @@ const PhotoFrame = ({
isFirstLoad, isFirstLoad,
openFileUploader, openFileUploader,
loadingBar, loadingBar,
searchMode,
}: Props) => { }: Props) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [currentIndex, setCurrentIndex] = useState<number>(0); const [currentIndex, setCurrentIndex] = useState<number>(0);
@ -274,7 +276,7 @@ const PhotoFrame = ({
return ( return (
<> <>
{!isFirstLoad && files.length == 0 ? ( {!isFirstLoad && files.length == 0 && !searchMode ? (
<EmptyScreen> <EmptyScreen>
<CloudUpload width={150} height={150} /> <CloudUpload width={150} height={150} />
<Button <Button
@ -365,6 +367,7 @@ const PhotoFrame = ({
} }
}); });
files.length < 30 && files.length < 30 &&
!searchMode &&
timeStampList.push({ timeStampList.push({
itemType: ITEM_TYPE.BANNER, itemType: ITEM_TYPE.BANNER,
banner: ( banner: (

View file

@ -0,0 +1,335 @@
import { SetCollections, SetFiles } from 'pages/gallery';
import React, { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';
import AsyncSelect from 'react-select/async';
import { components } from 'react-select';
import debounce from 'debounce-promise';
import { File, getLocalFiles } from 'services/fileService';
import {
Collection,
getLocalCollections,
getNonEmptyCollections,
} from 'services/collectionService';
import { Bbox, parseHumanDate, searchLocation } from 'services/searchService';
import {
getFilesWithCreationDay,
getFilesInsideBbox,
getFormattedDate,
getDefaultSuggestions,
} from 'utils/search';
import constants from 'utils/strings/constants';
import LocationIcon from './LocationIcon';
import DateIcon from './DateIcon';
import SearchIcon from './SearchIcon';
import CrossIcon from './CrossIcon';
const Wrapper = styled.div<{ width: number }>`
position: fixed;
z-index: 1000;
top: 0;
left: ${(props) => `max(0px, 50% - min(360px,${props.width / 2}px))`};
width: 100%;
max-width: 720px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 5%;
background-color: #111;
color: #fff;
min-height: 64px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
margin-bottom: 10px;
`;
const SearchButton = styled.div`
top: 1px;
z-index: 100;
right: 80px;
color: #fff;
cursor: pointer;
min-height: 64px;
position: fixed;
display: flex;
align-items: center;
justify-content: center;
`;
const SearchStats = styled.div`
display: flex;
justify-content: center;
align-items: center;
color: #979797;
`;
export enum SuggestionType {
DATE,
LOCATION,
}
export interface Suggestion {
type: SuggestionType;
label: string;
value: Bbox | Date;
}
interface Props {
isOpen: boolean;
isFirstLoad: boolean;
setOpen: (value) => void;
loadingBar: any;
setFiles: SetFiles;
setCollections: SetCollections;
}
interface Stats {
resultCount: number;
timeTaken: number;
}
export default function SearchBar(props: Props) {
const [allFiles, setAllFiles] = useState<File[]>([]);
const [allCollections, setAllCollections] = useState<Collection[]>([]);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
const [stats, setStats] = useState<Stats>(null);
const selectRef = useRef(null);
useEffect(() => {
if (props.isOpen) {
setTimeout(() => {
selectRef.current?.focus();
}, 250);
}
if (!props.isOpen && allFiles?.length > 0) {
return;
}
const main = async () => {
setAllFiles(await getLocalFiles());
setAllCollections(await getLocalCollections());
};
main();
}, [props.isOpen]);
useEffect(() => {
window.addEventListener('resize', () =>
setWindowWidth(window.innerWidth)
);
});
//==========================
// Functionality
//==========================
const getAutoCompleteSuggestions = async (searchPhrase: string) => {
let option = getDefaultSuggestions().filter((suggestion) =>
suggestion.label.toLowerCase().includes(searchPhrase.toLowerCase())
);
if (!searchPhrase?.length) {
return option;
}
const searchedDate = parseHumanDate(searchPhrase);
if (searchedDate != null) {
option.push({
type: SuggestionType.DATE,
value: searchedDate,
label: getFormattedDate(searchedDate),
});
}
const searchResults = await searchLocation(searchPhrase);
option.push(
...searchResults.map(
(searchResult) =>
({
type: SuggestionType.LOCATION,
value: searchResult.bbox,
label: searchResult.place,
} as Suggestion)
)
);
return option;
};
const getOptions = debounce(getAutoCompleteSuggestions, 250);
const filterFiles = (selectedOption: Suggestion) => {
if (!selectedOption) {
return;
}
const startTime = Date.now();
props.setOpen(true);
let resultFiles: File[] = [];
switch (selectedOption.type) {
case SuggestionType.DATE:
const searchedDate = selectedOption.value as Date;
const filesWithSameDate = getFilesWithCreationDay(
allFiles,
searchedDate
);
resultFiles = filesWithSameDate;
break;
case SuggestionType.LOCATION:
const bbox = selectedOption.value as Bbox;
const filesTakenAtLocation = getFilesInsideBbox(allFiles, bbox);
resultFiles = filesTakenAtLocation;
}
props.setFiles(resultFiles);
props.setCollections(
getNonEmptyCollections(allCollections, resultFiles)
);
const timeTaken = (Date.now() - startTime) / 1000;
setStats({
timeTaken,
resultCount: resultFiles.length,
});
};
const resetSearch = () => {
if (props.isOpen) {
selectRef.current.select.state.value = null;
props.loadingBar.current?.continuousStart();
props.setFiles(allFiles);
props.setCollections(allCollections);
setTimeout(() => {
props.loadingBar.current?.complete();
}, 10);
props.setOpen(false);
setStats(null);
}
};
//==========================
// UI
//==========================
const getIconByType = (type: SuggestionType) =>
type === SuggestionType.DATE ? <DateIcon /> : <LocationIcon />;
const LabelWithIcon = (props: { type: SuggestionType; label: string }) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
<span style={{ paddingRight: '10px', paddingBottom: '4px' }}>
{getIconByType(props.type)}
</span>
<span>{props.label}</span>
</div>
);
const { Option, Control } = components;
const OptionWithIcon = (props) => (
<Option {...props}>
<LabelWithIcon type={props.data.type} label={props.data.label} />
</Option>
);
const ControlWithIcon = (props) => (
<Control {...props}>
<span
className={'icon'}
style={{
paddingLeft: '10px',
paddingBottom: '4px',
}}
>
{props.getValue().length == 0 || props.menuIsOpen ? (
<SearchIcon />
) : props.getValue()[0].type == SuggestionType.DATE ? (
<DateIcon />
) : (
<LocationIcon />
)}
</span>
{props.children}
</Control>
);
const customStyles = {
control: (style, { isFocused }) => ({
...style,
backgroundColor: '#282828',
color: '#d1d1d1',
borderColor: isFocused ? '#2dc262' : '#444',
boxShadow: isFocused && '0 0 3px #2dc262',
':hover': {
borderColor: '#2dc262',
cursor: 'text',
'&>.icon': { color: '#2dc262' },
},
}),
input: (style) => ({
...style,
color: '#d1d1d1',
}),
menu: (style) => ({
...style,
marginTop: '10px',
backgroundColor: '#282828',
}),
option: (style, { isFocused }) => ({
...style,
backgroundColor: isFocused && '#343434',
}),
dropdownIndicator: (style) => ({
...style,
display: 'none',
}),
indicatorSeparator: (style) => ({
...style,
display: 'none',
}),
clearIndicator: (style) => ({
...style,
display: 'none',
}),
singleValue: (style, state) => ({
...style,
backgroundColor: '#282828',
color: '#d1d1d1',
display: state.selectProps.menuIsOpen ? 'none' : 'block',
}),
placeholder: (style) => ({
...style,
color: '#686868',
wordSpacing: '2px',
}),
};
return (
!props.isFirstLoad && (
<>
{windowWidth > 1000 || props.isOpen ? (
<Wrapper width={windowWidth}>
<div
style={{
flex: 1,
margin: '10px',
}}
>
<AsyncSelect
components={{
Option: OptionWithIcon,
Control: ControlWithIcon,
}}
ref={selectRef}
placeholder={constants.SEARCH_HINT()}
loadOptions={getOptions}
onChange={filterFiles}
isClearable
escapeClearsValue
styles={customStyles}
noOptionsMessage={() => null}
/>
</div>
<div style={{ width: '24px' }}>
{props.isOpen && (
<div
style={{ cursor: 'pointer' }}
onClick={resetSearch}
>
<CrossIcon />
</div>
)}
</div>
</Wrapper>
) : (
<SearchButton onClick={() => props.setOpen(true)}>
<SearchIcon />
</SearchButton>
)}
{props.isOpen && stats && (
<SearchStats>{constants.SEARCH_STATS(stats)}</SearchStats>
)}
</>
)
);
}

View file

@ -0,0 +1,21 @@
import React from 'react';
export default function SearchIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
<path d="M20.49 19l-5.73-5.73C15.53 12.2 16 10.91 16 9.5A6.5 6.5 0 1 0 9.5 16c1.41 0 2.7-.47 3.77-1.24L19 20.49 20.49 19zM5 9.5C5 7.01 7.01 5 9.5 5S14 7.01 14 9.5 11.99 14 9.5 14 5 11.99 5 9.5z"></path>
</svg>
);
}
SearchIcon.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
open: false,
};

View file

@ -27,6 +27,7 @@ import LinkButton from 'pages/gallery/components/LinkButton';
import { downloadApp } from 'utils/common'; import { downloadApp } from 'utils/common';
import { logoutUser } from 'services/userService'; import { logoutUser } from 'services/userService';
import { SetDialogMessage } from './MessageDialog'; import { SetDialogMessage } from './MessageDialog';
import { LogoImage } from 'pages/_app';
interface Props { interface Props {
files: File[]; files: File[];
@ -102,9 +103,15 @@ export default function Sidebar(props: Props) {
onStateChange={(state) => setIsOpen(state.isOpen)} onStateChange={(state) => setIsOpen(state.isOpen)}
itemListElement="div" itemListElement="div"
> >
<div style={{ display: 'flex', textAlign: 'center' }}>
<LogoImage
style={{ height: '24px', padding: '3px' }}
alt="logo"
src="/icon.svg"
/>
</div>
<div <div
style={{ style={{
marginBottom: '8px',
outline: 'none', outline: 'none',
color: 'rgb(45, 194, 98)', color: 'rgb(45, 194, 98)',
fontSize: '16px', fontSize: '16px',
@ -112,7 +119,7 @@ export default function Sidebar(props: Props) {
> >
{user?.email} {user?.email}
</div> </div>
<div style={{ flex: 1, overflow: 'auto' }}> <div style={{ flex: 1, overflow: 'auto', paddingTop: '0' }}>
<div style={{ outline: 'none' }}> <div style={{ outline: 'none' }}>
<div style={{ display: 'flex' }}> <div style={{ display: 'flex' }}>
<h5 style={{ margin: '4px 0 12px 2px' }}> <h5 style={{ margin: '4px 0 12px 2px' }}>

View file

@ -7,7 +7,7 @@ import Container from 'components/Container';
import Head from 'next/head'; import Head from 'next/head';
import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/css/bootstrap.min.css';
import 'photoswipe/dist/photoswipe.css'; import 'photoswipe/dist/photoswipe.css';
import { Workbox } from "workbox-window"; import { Workbox } from 'workbox-window';
import { sentryInit } from '../utils/sentry'; import { sentryInit } from '../utils/sentry';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
@ -183,8 +183,9 @@ const GlobalStyles = createGlobalStyle`
position: fixed; position: fixed;
width: 24px; width: 24px;
height: 16px; height: 16px;
top:25px; top:27px;
left: 32px; left: 32px;
z-index:100 !important;
} }
.bm-burger-bars { .bm-burger-bars {
background: #bdbdbd; background: #bdbdbd;
@ -281,7 +282,7 @@ const GlobalStyles = createGlobalStyle`
} }
`; `;
const Image = styled.img` export const LogoImage = styled.img`
max-height: 28px; max-height: 28px;
margin-right: 5px; margin-right: 5px;
`; `;
@ -308,17 +309,19 @@ sentryInit();
export default function App({ Component, pageProps, err }) { export default function App({ Component, pageProps, err }) {
const router = useRouter(); const router = useRouter();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [offline, setOffline] = useState(typeof window !== 'undefined' && !window.navigator.onLine); const [offline, setOffline] = useState(
typeof window !== 'undefined' && !window.navigator.onLine
);
useEffect(() => { useEffect(() => {
if ( if (
!("serviceWorker" in navigator) || !('serviceWorker' in navigator) ||
process.env.NODE_ENV !== "production" process.env.NODE_ENV !== 'production'
) { ) {
console.warn("Progressive Web App support is disabled"); console.warn('Progressive Web App support is disabled');
return; return;
} }
const wb = new Workbox("sw.js", { scope: "/" }); const wb = new Workbox('sw.js', { scope: '/' });
wb.register(); wb.register();
}, []); }, []);
@ -348,8 +351,7 @@ export default function App({ Component, pageProps, err }) {
return () => { return () => {
window.removeEventListener('online', setUserOnline); window.removeEventListener('online', setUserOnline);
window.removeEventListener('offline', setUserOffline); window.removeEventListener('offline', setUserOffline);
} };
}, []); }, []);
return ( return (
@ -367,14 +369,16 @@ export default function App({ Component, pageProps, err }) {
<GlobalStyles /> <GlobalStyles />
<Navbar> <Navbar>
<FlexContainer> <FlexContainer>
<Image <LogoImage
style={{ height: '24px', padding: '3px' }} style={{ height: '24px', padding: '3px' }}
alt="logo" alt="logo"
src="/icon.svg" src="/icon.svg"
/> />
</FlexContainer> </FlexContainer>
</Navbar> </Navbar>
{offline && <OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>} {offline && (
<OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>
)}
{loading ? ( {loading ? (
<Container> <Container>
<EnteSpinner> <EnteSpinner>

View file

@ -36,8 +36,13 @@ export default class MyDocument extends Document {
name="description" name="description"
content="ente is a privacy focussed photo storage service that offers end-to-end encryption." content="ente is a privacy focussed photo storage service that offers end-to-end encryption."
/> />
<link rel="icon" href="/icon.svg" type="image/png" /> <link rel="icon" href="/images/favicon.png" type="image/png" />
<link rel="manifest" href="manifest.json"></link> <link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="/images/ente-512.png" />
<meta name="theme-color" content="#111" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
</Head> </Head>
<body> <body>
<Main /> <Main />

View file

@ -25,7 +25,7 @@ interface CollectionProps {
} }
const Container = styled.div` const Container = styled.div`
margin: 0 auto; margin: 10px auto;
overflow-y: hidden; overflow-y: hidden;
height: 50px; height: 50px;
display: flex; display: flex;
@ -80,15 +80,18 @@ export default function Collections(props: CollectionProps) {
const [collectionShareModalView, setCollectionShareModalView] = const [collectionShareModalView, setCollectionShareModalView] =
useState(false); useState(false);
const [scrollObj, setScrollObj] = useState<{ const [scrollObj, setScrollObj] = useState<{
scrollLeft?: number, scrollWidth?: number, clientWidth?: number scrollLeft?: number;
scrollWidth?: number;
clientWidth?: number;
}>({}); }>({});
const updateScrollObj = () => { const updateScrollObj = () => {
if (collectionRef.current) { if (collectionRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = collectionRef.current; const { scrollLeft, scrollWidth, clientWidth } =
collectionRef.current;
setScrollObj({ scrollLeft, scrollWidth, clientWidth }); setScrollObj({ scrollLeft, scrollWidth, clientWidth });
} }
} };
useEffect(() => { useEffect(() => {
updateScrollObj(); updateScrollObj();
@ -125,7 +128,7 @@ export default function Collections(props: CollectionProps) {
const scrollCollection = (direction: SCROLL_DIRECTION) => () => { const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
collectionRef.current.scrollBy(250 * direction, 0); collectionRef.current.scrollBy(250 * direction, 0);
} };
return ( return (
<> <>
@ -139,10 +142,12 @@ export default function Collections(props: CollectionProps) {
syncWithRemote={props.syncWithRemote} syncWithRemote={props.syncWithRemote}
/> />
<Container> <Container>
{scrollObj.scrollLeft > 0 && <NavigationButton {scrollObj.scrollLeft > 0 && (
<NavigationButton
scrollDirection={SCROLL_DIRECTION.LEFT} scrollDirection={SCROLL_DIRECTION.LEFT}
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)} onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
/>} />
)}
<Wrapper ref={collectionRef} onScroll={updateScrollObj}> <Wrapper ref={collectionRef} onScroll={updateScrollObj}>
<Chip active={!selected} onClick={clickHandler()}> <Chip active={!selected} onClick={clickHandler()}>
All All
@ -182,10 +187,13 @@ export default function Collections(props: CollectionProps) {
</Chip> </Chip>
))} ))}
</Wrapper> </Wrapper>
{scrollObj.scrollLeft < (scrollObj.scrollWidth - scrollObj.clientWidth) && <NavigationButton {scrollObj.scrollLeft <
scrollObj.scrollWidth - scrollObj.clientWidth && (
<NavigationButton
scrollDirection={SCROLL_DIRECTION.RIGHT} scrollDirection={SCROLL_DIRECTION.RIGHT}
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)} onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
/>} />
)}
</Container> </Container>
</> </>
); );

View file

@ -1,17 +1,19 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components';
const Wrapper = styled.div`
position: fixed;
display: flex;
align-items: center;
justify-content: center;
top: 0;
z-index: 100;
min-height: 64px;
right: 32px;
`;
function UploadButton({ openFileUploader }) { function UploadButton({ openFileUploader }) {
return ( return (
<div <Wrapper onClick={openFileUploader}>
onClick={openFileUploader}
style={{
position: 'absolute',
right: '30px',
top: '20px',
zIndex: 100,
cursor: 'pointer',
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -25,7 +27,7 @@ function UploadButton({ openFileUploader }) {
d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10zM8 13.01l1.41 1.41L11 12.84V17h2v-4.16l1.59 1.59L16 13.01 12.01 9 8 13.01z" d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10zM8 13.01l1.41 1.41L11 12.84V17h2v-4.16l1.59 1.59L16 13.01 12.01 9 8 13.01z"
/> />
</svg> </svg>
</div> </Wrapper>
); );
} }

View file

@ -36,7 +36,7 @@ import {
setIsFirstLogin, setIsFirstLogin,
setJustSignedUp, setJustSignedUp,
} from 'utils/storage'; } from 'utils/storage';
import { logoutUser } from 'services/userService'; import { isTokenValid, logoutUser } from 'services/userService';
import AlertBanner from './components/AlertBanner'; import AlertBanner from './components/AlertBanner';
import MessageDialog, { MessageAttributes } from 'components/MessageDialog'; import MessageDialog, { MessageAttributes } from 'components/MessageDialog';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
@ -53,6 +53,7 @@ import { getSelectedFileIds } from 'utils/file';
import { addFilesToCollection } from 'utils/collection'; import { addFilesToCollection } from 'utils/collection';
import SelectedFileOptions from './components/SelectedFileOptions'; import SelectedFileOptions from './components/SelectedFileOptions';
import { errorCodes } from 'utils/common/errorUtil'; import { errorCodes } from 'utils/common/errorUtil';
import SearchBar from 'components/SearchBar';
export enum FILE_TYPE { export enum FILE_TYPE {
IMAGE, IMAGE,
@ -74,6 +75,8 @@ export type selectedState = {
[k: number]: boolean; [k: number]: boolean;
count: number; count: number;
}; };
export type SetFiles = React.Dispatch<React.SetStateAction<File[]>>;
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
export type SetLoading = React.Dispatch<React.SetStateAction<Boolean>>; export type SetLoading = React.Dispatch<React.SetStateAction<Boolean>>;
export default function Gallery() { export default function Gallery() {
@ -110,6 +113,7 @@ export default function Gallery() {
}); });
const loadingBar = useRef(null); const loadingBar = useRef(null);
const [searchMode, setSearchMode] = useState(false);
useEffect(() => { useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) { if (!key) {
@ -156,6 +160,9 @@ export default function Gallery() {
const syncWithRemote = async () => { const syncWithRemote = async () => {
try { try {
checkConnectivity(); checkConnectivity();
if (!(await isTokenValid())) {
throw new Error(errorCodes.ERR_SESSION_EXPIRED);
}
loadingBar.current?.continuousStart(); loadingBar.current?.continuousStart();
await billingService.updatePlans(); await billingService.updatePlans();
await billingService.syncSubscription(); await billingService.syncSubscription();
@ -194,6 +201,9 @@ export default function Gallery() {
nonClosable: true, nonClosable: true,
}); });
break; break;
case errorCodes.ERR_NO_INTERNET_CONNECTION:
setBannerMessage(constants.NO_INTERNET_CONNECTION);
break;
case errorCodes.ERR_KEY_MISSING: case errorCodes.ERR_KEY_MISSING:
clearKeys(); clearKeys();
router.push('/credentials'); router.push('/credentials');
@ -250,6 +260,11 @@ export default function Gallery() {
syncWithRemote syncWithRemote
); );
}; };
const updateFiles = (files: File[]) => {
setFiles(files);
setSinceTime(new Date().getTime());
selectCollection(null);
};
return ( return (
<FullScreenDropZone <FullScreenDropZone
getRootProps={getRootProps} getRootProps={getRootProps}
@ -282,6 +297,14 @@ export default function Gallery() {
onHide={() => setDialogView(false)} onHide={() => setDialogView(false)}
attributes={dialogMessage} attributes={dialogMessage}
/> />
<SearchBar
isOpen={searchMode}
setOpen={setSearchMode}
loadingBar={loadingBar}
isFirstLoad={isFirstLoad}
setFiles={updateFiles}
setCollections={setCollections}
/>
<Collections <Collections
collections={collections} collections={collections}
selected={Number(router.query.collection)} selected={Number(router.query.collection)}
@ -339,6 +362,7 @@ export default function Gallery() {
isFirstLoad={isFirstLoad} isFirstLoad={isFirstLoad}
openFileUploader={openFileUploader} openFileUploader={openFileUploader}
loadingBar={loadingBar} loadingBar={loadingBar}
searchMode={searchMode}
/> />
{selected.count > 0 && ( {selected.count > 0 && (
<SelectedFileOptions <SelectedFileOptions

View file

@ -13,23 +13,6 @@ interface IQueryPrams {
* Service to manage all HTTP calls. * Service to manage all HTTP calls.
*/ */
class HTTPService { class HTTPService {
constructor() {
axios.interceptors.response.use(
(response) => {
return Promise.resolve(response);
},
(err) => {
if (!err.response) {
return Promise.reject(err);
}
const response = err.response;
if (response?.status === 401) {
clearData();
}
return Promise.reject(response);
}
);
}
/** /**
* header object to be append to all api calls. * header object to be append to all api calls.
*/ */

View file

@ -125,7 +125,7 @@ const getCollections = async (
return await Promise.all(promises); return await Promise.all(promises);
} catch (e) { } catch (e) {
console.error('getCollections failed- ', e); console.error('getCollections failed- ', e);
throw new Error(e?.status?.toString()); throw e;
} }
}; };
@ -333,10 +333,8 @@ export const addToCollection = async (
await Promise.all( await Promise.all(
files.map(async (file) => { files.map(async (file) => {
file.collectionID = collection.id; file.collectionID = collection.id;
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64( const newEncryptedKey: B64EncryptionResult =
file.key, await worker.encryptToB64(file.key, collection.key);
collection.key
);
file.encryptedKey = newEncryptedKey.encryptedData; file.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce; file.keyDecryptionNonce = newEncryptedKey.nonce;
if (params['files'] == undefined) { if (params['files'] == undefined) {

View file

@ -0,0 +1,35 @@
import HTTPService from './HTTPService';
import * as chrono from 'chrono-node';
import { getEndpoint } from 'utils/common/apiUtil';
import { getToken } from 'utils/common/key';
const ENDPOINT = getEndpoint();
export type Bbox = [number, number, number, number];
export interface LocationSearchResponse {
place: string;
bbox: Bbox;
}
export const getMapboxToken = () => {
return process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
};
export function parseHumanDate(humanDate: string) {
return chrono.parseDate(humanDate);
}
export async function searchLocation(
searchPhrase: string
): Promise<LocationSearchResponse[]> {
const resp = await HTTPService.get(
`${ENDPOINT}/search/location`,
{
query: searchPhrase,
limit: 4,
},
{
'X-Auth-Token': getToken(),
}
);
return resp.data.results;
}

72
src/utils/search/index.ts Normal file
View file

@ -0,0 +1,72 @@
import { Suggestion, SuggestionType } from 'components/SearchBar';
import { File } from 'services/fileService';
export function getFilesInsideBbox(
files: File[],
bbox: [number, number, number, number]
) {
return files.filter((file) => {
if (file.metadata.latitude == null && file.metadata.longitude == null) {
return false;
}
if (
file.metadata.longitude >= bbox[0] &&
file.metadata.latitude >= bbox[1] &&
file.metadata.longitude <= bbox[2] &&
file.metadata.latitude <= bbox[3]
) {
return true;
}
});
}
const isSameDay = (baseDate) => (compareDate) => {
return (
baseDate.getMonth() === compareDate.getMonth() &&
baseDate.getDate() === compareDate.getDate()
);
};
export function getFilesWithCreationDay(files: File[], searchedDate: Date) {
const isSearchedDate = isSameDay(searchedDate);
return files.filter((file) =>
isSearchedDate(new Date(file.metadata.creationTime / 1000))
);
}
export function getFormattedDate(date: Date) {
return new Intl.DateTimeFormat('en-IN', {
month: 'long',
day: 'numeric',
}).format(date);
}
export function getDefaultSuggestions() {
return [
{
label: 'Christmas',
value: new Date(2021, 11, 25),
type: SuggestionType.DATE,
},
{
label: 'Christmas Eve',
value: new Date(2021, 11, 24),
type: SuggestionType.DATE,
},
{
label: 'New Year',
value: new Date(2021, 0, 1),
type: SuggestionType.DATE,
},
{
label: 'New Year Eve',
value: new Date(2021, 11, 31),
type: SuggestionType.DATE,
},
{
label: "Valentine's Day",
value: new Date(2021, 1, 14),
type: SuggestionType.DATE,
},
] as Suggestion[];
}

View file

@ -301,6 +301,7 @@ const englishConstants = {
SHARING_BAD_REQUEST_ERROR: 'sharing album not allowed', SHARING_BAD_REQUEST_ERROR: 'sharing album not allowed',
SHARING_DISABLED_FOR_FREE_ACCOUNTS: 'sharing is disabled for free accounts', SHARING_DISABLED_FOR_FREE_ACCOUNTS: 'sharing is disabled for free accounts',
CREATE_ALBUM_FAILED: 'failed to create album , please try again', CREATE_ALBUM_FAILED: 'failed to create album , please try again',
SEARCH_HINT: () => <span>New York, April 14, Christmas...</span>,
TERMS_AND_CONDITIONS: () => ( TERMS_AND_CONDITIONS: () => (
<p> <p>
I agree to the{' '} I agree to the{' '}
@ -323,6 +324,13 @@ const englishConstants = {
with ente with ente
</p> </p>
), ),
SEARCH_STATS: ({ resultCount, timeTaken }) => (
<span>
found <span style={{ color: '#2dc262' }}>{resultCount}</span>{' '}
memories ( <span style={{ color: '#2dc262' }}> {timeTaken}</span>{' '}
seconds )
</span>
),
}; };
export default englishConstants; export default englishConstants;

2060
yarn.lock

File diff suppressed because it is too large Load diff