Merge pull request #236 from ente-io/master

Mid Nov Release
This commit is contained in:
abhinavkgrd 2021-11-18 10:21:58 +05:30 committed by GitHub
commit f63fbcc680
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 639 additions and 53 deletions

View file

@ -20,6 +20,7 @@
"@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0", "@typescript-eslint/parser": "^4.25.0",
"axios": "^0.21.3", "axios": "^0.21.3",
"bip39": "^3.0.4",
"bootstrap": "^4.5.2", "bootstrap": "^4.5.2",
"chrono-node": "^2.2.6", "chrono-node": "^2.2.6",
"comlink": "^4.3.0", "comlink": "^4.3.0",

View file

@ -2,16 +2,18 @@ import React from 'react';
import { Spinner } from 'react-bootstrap'; import { Spinner } from 'react-bootstrap';
export default function EnteSpinner(props) { export default function EnteSpinner(props) {
const { style, ...others } = props ?? {};
return ( return (
<Spinner <Spinner
{...props}
animation="border" animation="border"
style={{ style={{
width: '36px', width: '36px',
height: '36px', height: '36px',
borderWidth: '0.20em', borderWidth: '0.20em',
color: '#51cd7c', color: '#51cd7c',
...(style && style),
}} }}
{...others}
role="status" role="status"
/> />
); );

View file

@ -0,0 +1,172 @@
import constants from 'utils/strings/constants';
import MessageDialog from './MessageDialog';
import React, { useContext, useEffect, useState } from 'react';
import { ProgressBar, Button } from 'react-bootstrap';
import { ComfySpan } from './ExportInProgress';
import { updateCreationTimeWithExif } from 'services/updateCreationTimeWithExif';
import { GalleryContext } from 'pages/gallery';
import { File } from 'services/fileService';
export interface FixCreationTimeAttributes {
files: File[];
}
interface Props {
isOpen: boolean;
show: () => void;
hide: () => void;
attributes: FixCreationTimeAttributes;
}
export enum FIX_STATE {
NOT_STARTED,
RUNNING,
COMPLETED,
COMPLETED_WITH_ERRORS,
}
function Message(props: { fixState: FIX_STATE }) {
let message = null;
switch (props.fixState) {
case FIX_STATE.NOT_STARTED:
message = constants.UPDATE_CREATION_TIME_NOT_STARTED();
break;
case FIX_STATE.COMPLETED:
message = constants.UPDATE_CREATION_TIME_COMPLETED();
break;
case FIX_STATE.COMPLETED_WITH_ERRORS:
message = constants.UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR();
break;
}
return message ? <div>{message}</div> : <></>;
}
export default function FixCreationTime(props: Props) {
const [fixState, setFixState] = useState(FIX_STATE.NOT_STARTED);
const [progressTracker, setProgressTracker] = useState({
current: 0,
total: 0,
});
const galleryContext = useContext(GalleryContext);
useEffect(() => {
if (
props.attributes &&
props.isOpen &&
fixState !== FIX_STATE.RUNNING
) {
setFixState(FIX_STATE.NOT_STARTED);
}
}, [props.isOpen]);
const startFix = async () => {
setFixState(FIX_STATE.RUNNING);
const completedWithoutError = await updateCreationTimeWithExif(
props.attributes.files,
setProgressTracker
);
if (!completedWithoutError) {
setFixState(FIX_STATE.COMPLETED);
} else {
setFixState(FIX_STATE.COMPLETED_WITH_ERRORS);
}
await galleryContext.syncWithRemote();
};
if (!props.attributes) {
return <></>;
}
return (
<MessageDialog
show={props.isOpen}
onHide={props.hide}
attributes={{
title:
fixState === FIX_STATE.RUNNING
? constants.FIX_CREATION_TIME_IN_PROGRESS
: constants.FIX_CREATION_TIME,
staticBackdrop: true,
nonClosable: true,
}}>
<div
style={{
marginBottom: '10px',
padding: '0 5%',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}>
<Message fixState={fixState} />
{fixState === FIX_STATE.RUNNING && (
<>
<div style={{ marginBottom: '10px' }}>
<ComfySpan>
{' '}
{progressTracker.current} /{' '}
{progressTracker.total}{' '}
</ComfySpan>{' '}
<span style={{ marginLeft: '10px' }}>
{' '}
{constants.CREATION_TIME_UPDATED}
</span>
</div>
<div
style={{
width: '100%',
marginTop: '10px',
marginBottom: '20px',
}}>
<ProgressBar
now={Math.round(
(progressTracker.current * 100) /
progressTracker.total
)}
animated={true}
variant="upload-progress-bar"
/>
</div>
</>
)}
{fixState !== FIX_STATE.RUNNING && (
<div
style={{
width: '100%',
display: 'flex',
marginTop: '30px',
justifyContent: 'space-around',
}}>
{(fixState === FIX_STATE.NOT_STARTED ||
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
<Button
block
variant={'outline-secondary'}
onClick={() => {
props.hide();
}}>
{constants.CANCEL}
</Button>
)}
{fixState === FIX_STATE.COMPLETED && (
<Button
block
variant={'outline-secondary'}
onClick={props.hide}>
{constants.CLOSE}
</Button>
)}
{(fixState === FIX_STATE.NOT_STARTED ||
fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && (
<>
<div style={{ width: '30px' }} />
<Button
block
variant={'outline-success'}
onClick={startFix}>
{constants.FIX_CREATION_TIME}
</Button>
</>
)}
</div>
)}
</div>
</MessageDialog>
);
}

View file

@ -189,7 +189,8 @@ export default function FixLargeThumbnails(props: Props) {
display: 'flex', display: 'flex',
justifyContent: 'space-around', justifyContent: 'space-around',
}}> }}>
{fixState === FIX_STATE.NOT_STARTED ? ( {fixState === FIX_STATE.NOT_STARTED ||
fixState === FIX_STATE.FIX_LATER ? (
<Button <Button
block block
variant={'outline-secondary'} variant={'outline-secondary'}
@ -197,7 +198,7 @@ export default function FixLargeThumbnails(props: Props) {
updateFixState(FIX_STATE.FIX_LATER); updateFixState(FIX_STATE.FIX_LATER);
props.hide(); props.hide();
}}> }}>
{constants.FIX_LATER} {constants.FIX_THUMBNAIL_LATER}
</Button> </Button>
) : ( ) : (
<Button <Button
@ -217,7 +218,7 @@ export default function FixLargeThumbnails(props: Props) {
block block
variant={'outline-success'} variant={'outline-success'}
onClick={() => startFix()}> onClick={() => startFix()}>
{constants.FIX} {constants.FIX_THUMBNAIL}
</Button> </Button>
</> </>
)} )}

View file

@ -10,6 +10,7 @@ import {
import { import {
ALL_TIME, ALL_TIME,
File, File,
MAX_EDITED_FILE_NAME_LENGTH,
MAX_EDITED_CREATION_TIME, MAX_EDITED_CREATION_TIME,
MIN_EDITED_CREATION_TIME, MIN_EDITED_CREATION_TIME,
updatePublicMagicMetadata, updatePublicMagicMetadata,
@ -22,20 +23,32 @@ import styled from 'styled-components';
import events from './events'; import events from './events';
import { import {
changeFileCreationTime, changeFileCreationTime,
changeFileName,
downloadFile, downloadFile,
formatDateTime, formatDateTime,
splitFilenameAndExtension,
updateExistingFilePubMetadata, updateExistingFilePubMetadata,
} from 'utils/file'; } from 'utils/file';
import { FormCheck } from 'react-bootstrap'; import { Col, Form, FormCheck, FormControl } from 'react-bootstrap';
import { prettyPrintExif } from 'utils/exif'; import { prettyPrintExif } from 'utils/exif';
import EditIcon from 'components/icons/EditIcon'; import EditIcon from 'components/icons/EditIcon';
import { IconButton, Label, Row, Value } from 'components/Container'; import {
FlexWrapper,
IconButton,
Label,
Row,
Value,
} from 'components/Container';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import CloseIcon from 'components/icons/CloseIcon'; import CloseIcon from 'components/icons/CloseIcon';
import TickIcon from 'components/icons/TickIcon'; import TickIcon from 'components/icons/TickIcon';
import { FreeFlowText } from 'components/RecoveryKeyModal';
import { Formik } from 'formik';
import * as Yup from 'yup';
import EnteSpinner from 'components/EnteSpinner';
interface Iprops { interface Iprops {
isOpen: boolean; isOpen: boolean;
@ -86,7 +99,7 @@ function RenderCreationTime({
file: File; file: File;
scheduleUpdate: () => void; scheduleUpdate: () => void;
}) { }) {
const originalCreationTime = new Date(file.metadata.creationTime / 1000); const originalCreationTime = new Date(file?.metadata.creationTime / 1000);
const [isInEditMode, setIsInEditMode] = useState(false); const [isInEditMode, setIsInEditMode] = useState(false);
const [pickedTime, setPickedTime] = useState(originalCreationTime); const [pickedTime, setPickedTime] = useState(originalCreationTime);
@ -98,7 +111,8 @@ function RenderCreationTime({
try { try {
if (isInEditMode && file) { if (isInEditMode && file) {
const unixTimeInMicroSec = pickedTime.getTime() * 1000; const unixTimeInMicroSec = pickedTime.getTime() * 1000;
if (unixTimeInMicroSec === file.metadata.creationTime) { if (unixTimeInMicroSec === file?.metadata.creationTime) {
closeEditMode();
return; return;
} }
let updatedFile = await changeFileCreationTime( let updatedFile = await changeFileCreationTime(
@ -175,6 +189,170 @@ function RenderCreationTime({
</> </>
); );
} }
const getFileTitle = (filename, extension) => {
if (extension) {
return filename + '.' + extension;
} else {
return filename;
}
};
interface formValues {
filename: string;
}
const FileNameEditForm = ({ filename, saveEdits, discardEdits, extension }) => {
const [loading, setLoading] = useState(false);
const onSubmit = async (values: formValues) => {
try {
setLoading(true);
await saveEdits(values.filename);
} finally {
setLoading(false);
}
};
return (
<Formik<formValues>
initialValues={{ filename }}
validationSchema={Yup.object().shape({
filename: Yup.string()
.required(constants.REQUIRED)
.max(
MAX_EDITED_FILE_NAME_LENGTH,
constants.FILE_NAME_CHARACTER_LIMIT
),
})}
validateOnBlur={false}
onSubmit={onSubmit}>
{({ values, errors, handleChange, handleSubmit }) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Row>
<Form.Group
bsPrefix="ente-form-group"
as={Col}
xs={extension ? 7 : 8}>
<Form.Control
as="textarea"
placeholder={constants.FILE_NAME}
value={values.filename}
onChange={handleChange('filename')}
isInvalid={Boolean(errors.filename)}
autoFocus
disabled={loading}
/>
<FormControl.Feedback
type="invalid"
style={{ textAlign: 'center' }}>
{errors.filename}
</FormControl.Feedback>
</Form.Group>
{extension && (
<Form.Group
bsPrefix="ente-form-group"
as={Col}
xs={1}
controlId="formHorizontalFileName">
<FlexWrapper style={{ padding: '5px' }}>
{`.${extension}`}
</FlexWrapper>
</Form.Group>
)}
<Form.Group bsPrefix="ente-form-group" as={Col} xs={2}>
<Value width={'16.67%'}>
<IconButton type="submit" disabled={loading}>
{loading ? (
<EnteSpinner
style={{
width: '20px',
height: '20px',
}}
/>
) : (
<TickIcon />
)}
</IconButton>
<IconButton
onClick={discardEdits}
disabled={loading}>
<CloseIcon />
</IconButton>
</Value>
</Form.Group>
</Form.Row>
</Form>
)}
</Formik>
);
};
function RenderFileName({
file,
scheduleUpdate,
}: {
file: File;
scheduleUpdate: () => void;
}) {
const originalTitle = file?.metadata.title;
const [isInEditMode, setIsInEditMode] = useState(false);
const [originalFileName, extension] =
splitFilenameAndExtension(originalTitle);
const [filename, setFilename] = useState(originalFileName);
const openEditMode = () => setIsInEditMode(true);
const closeEditMode = () => setIsInEditMode(false);
const saveEdits = async (newFilename: string) => {
try {
if (file) {
if (filename === newFilename) {
closeEditMode();
return;
}
setFilename(newFilename);
const newTitle = getFileTitle(newFilename, extension);
let updatedFile = await changeFileName(file, newTitle);
updatedFile = (
await updatePublicMagicMetadata([updatedFile])
)[0];
updateExistingFilePubMetadata(file, updatedFile);
scheduleUpdate();
}
} catch (e) {
logError(e, 'failed to update file name');
} finally {
closeEditMode();
}
};
return (
<>
<Row>
<Label width="30%">{constants.FILE_NAME}</Label>
{!isInEditMode ? (
<>
<Value width="60%">
<FreeFlowText>
{getFileTitle(filename, extension)}
</FreeFlowText>
</Value>
<Value
width="10%"
style={{ cursor: 'pointer', marginLeft: '10px' }}>
<IconButton onClick={openEditMode}>
<EditIcon />
</IconButton>
</Value>
</>
) : (
<FileNameEditForm
extension={extension}
filename={filename}
saveEdits={saveEdits}
discardEdits={closeEditMode}
/>
)}
</Row>
</>
);
}
function ExifData(props: { exif: any }) { function ExifData(props: { exif: any }) {
const { exif } = props; const { exif } = props;
const [showAll, setShowAll] = useState(false); const [showAll, setShowAll] = useState(false);
@ -250,8 +428,12 @@ function InfoModal({
constants.FILE_ID, constants.FILE_ID,
items[photoSwipe?.getCurrentIndex()]?.id items[photoSwipe?.getCurrentIndex()]?.id
)} )}
{metadata?.title && {metadata?.title && (
renderInfoItem(constants.FILE_NAME, metadata.title)} <RenderFileName
file={items[photoSwipe?.getCurrentIndex()]}
scheduleUpdate={scheduleUpdate}
/>
)}
{metadata?.creationTime && ( {metadata?.creationTime && (
<RenderCreationTime <RenderCreationTime
file={items[photoSwipe?.getCurrentIndex()]} file={items[photoSwipe?.getCurrentIndex()]}

View file

@ -5,7 +5,9 @@ import constants from 'utils/strings/constants';
import MessageDialog from './MessageDialog'; import MessageDialog from './MessageDialog';
import EnteSpinner from './EnteSpinner'; import EnteSpinner from './EnteSpinner';
import styled from 'styled-components'; import styled from 'styled-components';
const bip39 = require('bip39');
// mobile client library only supports english.
bip39.setDefaultWordlist('english');
export const CodeBlock = styled.div<{ height: number }>` export const CodeBlock = styled.div<{ height: number }>`
display: flex; display: flex;
align-items: center; align-items: center;
@ -23,6 +25,7 @@ export const FreeFlowText = styled.div`
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
min-width: 30%; min-width: 30%;
text-align: left;
`; `;
interface Props { interface Props {
show: boolean; show: boolean;
@ -41,7 +44,7 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
somethingWentWrong(); somethingWentWrong();
props.onHide(); props.onHide();
} }
setRecoveryKey(recoveryKey); setRecoveryKey(bip39.entropyToMnemonic(recoveryKey));
}; };
main(); main();
}, [props.show]); }, [props.show]);

View file

@ -0,0 +1,20 @@
import React from 'react';
export default function ClockIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
{...props}>
<path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm1 12v-6h-2v8h7v-2h-5z" />
</svg>
);
}
ClockIcon.defaultProps = {
height: 20,
width: 20,
viewBox: '0 0 24 24',
};

View file

@ -1,5 +1,5 @@
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import React from 'react'; import React, { useEffect, useState } from 'react';
import { SetCollectionSelectorAttributes } from './CollectionSelector'; import { SetCollectionSelectorAttributes } from './CollectionSelector';
import styled from 'styled-components'; import styled from 'styled-components';
import Navbar from 'components/Navbar'; import Navbar from 'components/Navbar';
@ -17,6 +17,9 @@ import { OverlayTrigger } from 'react-bootstrap';
import { Collection } from 'services/collectionService'; import { Collection } from 'services/collectionService';
import RemoveIcon from 'components/icons/RemoveIcon'; import RemoveIcon from 'components/icons/RemoveIcon';
import RestoreIcon from 'components/icons/RestoreIcon'; import RestoreIcon from 'components/icons/RestoreIcon';
import ClockIcon from 'components/icons/ClockIcon';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { FIX_CREATION_TIME_USER_ID, User } from 'services/userService';
interface Props { interface Props {
addToCollectionHelper: (collection: Collection) => void; addToCollectionHelper: (collection: Collection) => void;
@ -27,6 +30,7 @@ interface Props {
setCollectionSelectorAttributes: SetCollectionSelectorAttributes; setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
deleteFileHelper: (permanent?: boolean) => void; deleteFileHelper: (permanent?: boolean) => void;
removeFromCollectionHelper: () => void; removeFromCollectionHelper: () => void;
fixTimeHelper: () => void;
count: number; count: number;
clearSelection: () => void; clearSelection: () => void;
archiveFilesHelper: () => void; archiveFilesHelper: () => void;
@ -68,6 +72,7 @@ const SelectedFileOptions = ({
restoreToCollectionHelper, restoreToCollectionHelper,
showCreateCollectionModal, showCreateCollectionModal,
removeFromCollectionHelper, removeFromCollectionHelper,
fixTimeHelper,
setDialogMessage, setDialogMessage,
setCollectionSelectorAttributes, setCollectionSelectorAttributes,
deleteFileHelper, deleteFileHelper,
@ -78,6 +83,12 @@ const SelectedFileOptions = ({
activeCollection, activeCollection,
isFavoriteCollection, isFavoriteCollection,
}: Props) => { }: Props) => {
const [showFixCreationTime, setShowFixCreationTime] = useState(false);
useEffect(() => {
const user: User = getData(LS_KEYS.USER);
const showFixCreationTime = user?.id === FIX_CREATION_TIME_USER_ID;
setShowFixCreationTime(showFixCreationTime);
}, []);
const addToCollection = () => const addToCollection = () =>
setCollectionSelectorAttributes({ setCollectionSelectorAttributes({
callback: addToCollectionHelper, callback: addToCollectionHelper,
@ -168,6 +179,18 @@ const SelectedFileOptions = ({
</> </>
) : ( ) : (
<> <>
{showFixCreationTime && (
<IconWithMessage message={constants.FIX_CREATION_TIME}>
<IconButton onClick={fixTimeHelper}>
<ClockIcon />
</IconButton>
</IconWithMessage>
)}
<IconWithMessage message={constants.ADD}>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
</IconWithMessage>
{activeCollection === ARCHIVE_SECTION && ( {activeCollection === ARCHIVE_SECTION && (
<IconWithMessage message={constants.UNARCHIVE}> <IconWithMessage message={constants.UNARCHIVE}>
<IconButton onClick={unArchiveFilesHelper}> <IconButton onClick={unArchiveFilesHelper}>
@ -182,11 +205,7 @@ const SelectedFileOptions = ({
</IconButton> </IconButton>
</IconWithMessage> </IconWithMessage>
)} )}
<IconWithMessage message={constants.ADD}>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
</IconWithMessage>
{activeCollection !== ALL_SECTION && {activeCollection !== ALL_SECTION &&
activeCollection !== ARCHIVE_SECTION && activeCollection !== ARCHIVE_SECTION &&
!isFavoriteCollection && ( !isFavoriteCollection && (

View file

@ -446,6 +446,9 @@ const GlobalStyles = createGlobalStyle`
.react-datepicker__day--disabled:hover { .react-datepicker__day--disabled:hover {
background-color: #202020; background-color: #202020;
} }
.ente-form-group{
margin:0;
}
`; `;
export const LogoImage = styled.img` export const LogoImage = styled.img`

View file

@ -93,6 +93,9 @@ import {
Trash, Trash,
} from 'services/trashService'; } from 'services/trashService';
import DeleteBtn from 'components/DeleteBtn'; import DeleteBtn from 'components/DeleteBtn';
import FixCreationTime, {
FixCreationTimeAttributes,
} from 'components/FixCreationTime';
export const DeadCenter = styled.div` export const DeadCenter = styled.div`
flex: 1; flex: 1;
@ -204,7 +207,9 @@ export default function Gallery() {
useState<Map<number, number>>(); useState<Map<number, number>>();
const [activeCollection, setActiveCollection] = useState<number>(undefined); const [activeCollection, setActiveCollection] = useState<number>(undefined);
const [trash, setTrash] = useState<Trash>([]); const [trash, setTrash] = useState<Trash>([]);
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
useState<FixCreationTimeAttributes>(null);
useEffect(() => { useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) { if (!key) {
@ -243,13 +248,19 @@ export default function Gallery() {
useEffect(() => setDialogView(true), [dialogMessage]); useEffect(() => setDialogView(true), [dialogMessage]);
useEffect(() => { useEffect(
if (collectionSelectorAttributes) { () => collectionSelectorAttributes && setCollectionSelectorView(true),
setCollectionSelectorView(true); [collectionSelectorAttributes]
} );
}, [collectionSelectorAttributes]);
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]); useEffect(
() => collectionNamerAttributes && setCollectionNamerView(true),
[collectionNamerAttributes]
);
useEffect(
() => fixCreationTimeAttributes && setFixCreationTimeView(true),
[fixCreationTimeAttributes]
);
useEffect(() => { useEffect(() => {
if (typeof activeCollection === 'undefined') { if (typeof activeCollection === 'undefined') {
@ -523,6 +534,12 @@ export default function Gallery() {
} }
}; };
const fixTimeHelper = async () => {
const selectedFiles = getSelectedFiles(selected, files);
setFixCreationTimeAttributes({ files: selectedFiles });
clearSelection();
};
return ( return (
<GalleryContext.Provider <GalleryContext.Provider
value={{ value={{
@ -594,6 +611,12 @@ export default function Gallery() {
} }
attributes={collectionSelectorAttributes} attributes={collectionSelectorAttributes}
/> />
<FixCreationTime
isOpen={fixCreationTimeView}
hide={() => setFixCreationTimeView(false)}
show={() => setFixCreationTimeView(true)}
attributes={fixCreationTimeAttributes}
/>
<Upload <Upload
syncWithRemote={syncWithRemote} syncWithRemote={syncWithRemote}
setBannerMessage={setBannerMessage} setBannerMessage={setBannerMessage}
@ -685,6 +708,7 @@ export default function Gallery() {
) )
) )
} }
fixTimeHelper={fixTimeHelper}
count={selected.count} count={selected.count}
clearSelection={clearSelection} clearSelection={clearSelection}
activeCollection={activeCollection} activeCollection={activeCollection}

View file

@ -21,6 +21,9 @@ import LogoImg from 'components/LogoImg';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { User } from 'services/userService'; import { User } from 'services/userService';
const bip39 = require('bip39');
// mobile client library only supports english.
bip39.setDefaultWordlist('english');
export default function Recover() { export default function Recover() {
const router = useRouter(); const router = useRouter();
@ -51,6 +54,13 @@ export default function Recover() {
const recover = async (recoveryKey: string, setFieldError) => { const recover = async (recoveryKey: string, setFieldError) => {
try { try {
// check if user is entering mnemonic recovery key
if (recoveryKey.trim().indexOf(' ') > 0) {
if (recoveryKey.trim().split(' ').length !== 24) {
throw new Error('recovery code should have 24 words');
}
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
}
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await new CryptoWorker();
const masterKey: string = await cryptoWorker.decryptB64( const masterKey: string = await cryptoWorker.decryptB64(
keyAttributes.masterKeyEncryptedWithRecoveryKey, keyAttributes.masterKeyEncryptedWithRecoveryKey,

View file

@ -12,6 +12,9 @@ import { logError } from 'utils/sentry';
import { recoverTwoFactor, removeTwoFactor } from 'services/userService'; import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app'; import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import { PAGES } from 'types'; import { PAGES } from 'types';
const bip39 = require('bip39');
// mobile client library only supports english.
bip39.setDefaultWordlist('english');
export default function Recover() { export default function Recover() {
const router = useRouter(); const router = useRouter();
@ -43,6 +46,13 @@ export default function Recover() {
const recover = async (recoveryKey: string, setFieldError) => { const recover = async (recoveryKey: string, setFieldError) => {
try { try {
// check if user is entering mnemonic recovery key
if (recoveryKey.trim().indexOf(' ') > 0) {
if (recoveryKey.trim().split(' ').length !== 24) {
throw new Error('recovery code should have 24 words');
}
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
}
const cryptoWorker = await new CryptoWorker(); const cryptoWorker = await new CryptoWorker();
const twoFactorSecret: string = await cryptoWorker.decryptB64( const twoFactorSecret: string = await cryptoWorker.decryptB64(
encryptedTwoFactorSecret.encryptedData, encryptedTwoFactorSecret.encryptedData,

View file

@ -33,12 +33,17 @@ class FFmpegService {
const response = this.generateThumbnailProcessor.queueUpRequest( const response = this.generateThumbnailProcessor.queueUpRequest(
generateThumbnailHelper.bind(null, this.ffmpeg, file) generateThumbnailHelper.bind(null, this.ffmpeg, file)
); );
try {
const thumbnail = await response.promise; return await response.promise;
if (!thumbnail) { } catch (e) {
throw Error(CustomError.THUMBNAIL_GENERATION_FAILED); if (e.message === CustomError.REQUEST_CANCELLED) {
// ignore
return null;
} else {
logError(e, 'ffmpeg thumbnail generation failed');
throw e;
}
} }
return thumbnail;
} }
} }

View file

@ -21,6 +21,8 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date(); export const MAX_EDITED_CREATION_TIME = new Date();
export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59); export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59);
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export interface fileAttribute { export interface fileAttribute {
encryptedData?: DataStream | Uint8Array; encryptedData?: DataStream | Uint8Array;
objectKey?: string; objectKey?: string;
@ -72,6 +74,7 @@ export interface MagicMetadata extends Omit<MagicMetadataCore, 'data'> {
export interface PublicMagicMetadataProps { export interface PublicMagicMetadataProps {
editedTime?: number; editedTime?: number;
editedName?: string;
} }
export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> { export interface PublicMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
@ -147,7 +150,7 @@ export const syncFiles = async (
let files = await removeDeletedCollectionFiles(collections, localFiles); let files = await removeDeletedCollectionFiles(collections, localFiles);
if (files.length !== localFiles.length) { if (files.length !== localFiles.length) {
await setLocalFiles(files); await setLocalFiles(files);
setFiles(sortFiles(mergeMetadata(files))); setFiles([...sortFiles(mergeMetadata(files))]);
} }
for (const collection of collections) { for (const collection of collections) {
if (!getToken()) { if (!getToken()) {
@ -183,9 +186,9 @@ export const syncFiles = async (
`${collection.id}-time`, `${collection.id}-time`,
collection.updationTime collection.updationTime
); );
setFiles(sortFiles(mergeMetadata(files))); setFiles([...sortFiles(mergeMetadata(files))]);
} }
return mergeMetadata(files); return sortFiles(mergeMetadata(files));
}; };
export const getFiles = async ( export const getFiles = async (

View file

@ -0,0 +1,62 @@
import { SetProgressTracker } from 'components/FixLargeThumbnail';
import CryptoWorker from 'utils/crypto';
import {
changeFileCreationTime,
getFileFromURL,
updateExistingFilePubMetadata,
} from 'utils/file';
import { logError } from 'utils/sentry';
import downloadManager from './downloadManager';
import { File, FILE_TYPE, updatePublicMagicMetadata } from './fileService';
import { getExifData } from './upload/exifService';
import { getFileType } from './upload/readFileService';
export async function updateCreationTimeWithExif(
filesToBeUpdated: File[],
setProgressTracker: SetProgressTracker
) {
let completedWithError = false;
try {
if (filesToBeUpdated.length === 0) {
return completedWithError;
}
setProgressTracker({ current: 0, total: filesToBeUpdated.length });
for (const [index, file] of filesToBeUpdated.entries()) {
try {
if (file.metadata.fileType !== FILE_TYPE.IMAGE) {
continue;
}
const fileURL = await downloadManager.getFile(file);
const fileObject = await getFileFromURL(fileURL);
const worker = await new CryptoWorker();
const fileTypeInfo = await getFileType(worker, fileObject);
const exifData = await getExifData(fileObject, fileTypeInfo);
if (
exifData?.creationTime &&
exifData?.creationTime !== file.metadata.creationTime
) {
let updatedFile = await changeFileCreationTime(
file,
exifData.creationTime
);
updatedFile = (
await updatePublicMagicMetadata([updatedFile])
)[0];
updateExistingFilePubMetadata(file, updatedFile);
}
} catch (e) {
logError(e, 'failed to updated a CreationTime With Exif');
completedWithError = true;
} finally {
setProgressTracker({
current: index + 1,
total: filesToBeUpdated.length,
});
}
}
} catch (e) {
logError(e, 'update CreationTime With Exif failed');
completedWithError = true;
}
return completedWithError;
}

View file

@ -1,6 +1,8 @@
import exifr from 'exifr'; import exifr from 'exifr';
import { logError } from 'utils/sentry';
import { NULL_LOCATION, Location } from './metadataService'; import { NULL_LOCATION, Location } from './metadataService';
import { FileTypeInfo } from './readFileService';
const EXIF_TAGS_NEEDED = [ const EXIF_TAGS_NEEDED = [
'DateTimeOriginal', 'DateTimeOriginal',
@ -17,9 +19,18 @@ interface ParsedEXIFData {
} }
export async function getExifData( export async function getExifData(
receivedFile: globalThis.File receivedFile: globalThis.File,
fileTypeInfo: FileTypeInfo
): Promise<ParsedEXIFData> { ): Promise<ParsedEXIFData> {
const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED); let exifData;
try {
exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
} catch (e) {
logError(e, 'file missing exif data ', {
fileType: fileTypeInfo.exactType,
});
// ignore exif parsing errors
}
if (!exifData) { if (!exifData) {
return { location: NULL_LOCATION, creationTime: null }; return { location: NULL_LOCATION, creationTime: null };
} }

View file

@ -34,14 +34,7 @@ export async function extractMetadata(
) { ) {
let exifData = null; let exifData = null;
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
try { exifData = await getExifData(receivedFile, fileTypeInfo);
exifData = await getExifData(receivedFile);
} catch (e) {
logError(e, 'file missing exif data ', {
fileType: fileTypeInfo.exactType,
});
// ignore exif parsing errors
}
} }
const extractedMetadata: MetadataObject = { const extractedMetadata: MetadataObject = {

View file

@ -1,6 +1,9 @@
import { CustomError } from 'utils/common/errorUtil';
interface RequestQueueItem { interface RequestQueueItem {
request: (canceller?: RequestCanceller) => Promise<any>; request: (canceller?: RequestCanceller) => Promise<any>;
callback: (response) => void; successCallback: (response: any) => void;
failureCallback: (error: Error) => void;
isCanceled: { status: boolean }; isCanceled: { status: boolean };
canceller: { exec: () => void }; canceller: { exec: () => void };
} }
@ -26,10 +29,11 @@ export default class QueueProcessor<T> {
}, },
}; };
const promise = new Promise<T>((resolve) => { const promise = new Promise<T>((resolve, reject) => {
this.requestQueue.push({ this.requestQueue.push({
request, request,
callback: resolve, successCallback: resolve,
failureCallback: reject,
isCanceled, isCanceled,
canceller, canceller,
}); });
@ -53,15 +57,15 @@ export default class QueueProcessor<T> {
let response = null; let response = null;
if (queueItem.isCanceled.status) { if (queueItem.isCanceled.status) {
response = null; queueItem.failureCallback(Error(CustomError.REQUEST_CANCELLED));
} else { } else {
try { try {
response = await queueItem.request(queueItem.canceller); response = await queueItem.request(queueItem.canceller);
queueItem.successCallback(response);
} catch (e) { } catch (e) {
response = null; queueItem.failureCallback(e);
} }
} }
queueItem.callback(response);
} }
} }
} }

View file

@ -56,7 +56,7 @@ export async function generateThumbnail(
} }
} catch (e) { } catch (e) {
logError(e, 'uploading static thumbnail', { logError(e, 'uploading static thumbnail', {
type: fileTypeInfo.exactType, fileFormat: fileTypeInfo.exactType,
}); });
thumbnail = Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) => thumbnail = Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) =>
c.charCodeAt(0) c.charCodeAt(0)

View file

@ -28,6 +28,8 @@ const ENDPOINT = getEndpoint();
const HAS_SET_KEYS = 'hasSetKeys'; const HAS_SET_KEYS = 'hasSetKeys';
export const FIX_CREATION_TIME_USER_ID = 341;
export interface User { export interface User {
id: number; id: number;
name: string; name: string;

View file

@ -28,6 +28,7 @@ export enum CustomError {
FAV_COLLECTION_MISSING = 'favorite collection missing', FAV_COLLECTION_MISSING = 'favorite collection missing',
INVALID_COLLECTION_OPERATION = 'invalid collection operation', INVALID_COLLECTION_OPERATION = 'invalid collection operation',
WAIT_TIME_EXCEEDED = 'thumbnail generation wait time exceeded', WAIT_TIME_EXCEEDED = 'thumbnail generation wait time exceeded',
REQUEST_CANCELLED = 'request canceled',
} }
function parseUploadError(error: AxiosResponse) { function parseUploadError(error: AxiosResponse) {

View file

@ -240,6 +240,16 @@ export function fileExtensionWithDot(filename) {
else return filename.substr(lastDotPosition); else return filename.substr(lastDotPosition);
} }
export function splitFilenameAndExtension(filename): [string, string] {
const lastDotPosition = filename.lastIndexOf('.');
if (lastDotPosition === -1) return [filename, null];
else
return [
filename.substr(0, lastDotPosition),
filename.substr(lastDotPosition + 1),
];
}
export function generateStreamFromArrayBuffer(data: Uint8Array) { export function generateStreamFromArrayBuffer(data: Uint8Array) {
return new ReadableStream({ return new ReadableStream({
async start(controller: ReadableStreamDefaultController) { async start(controller: ReadableStreamDefaultController) {
@ -381,6 +391,17 @@ export async function changeFileCreationTime(file: File, editedTime: number) {
); );
} }
export async function changeFileName(file: File, editedName: string) {
const updatedPublicMagicMetadataProps: PublicMagicMetadataProps = {
editedName,
};
return await updatePublicMagicMetadataProps(
file,
updatedPublicMagicMetadataProps
);
}
export function isSharedFile(file: File) { export function isSharedFile(file: File) {
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
@ -400,6 +421,9 @@ export function mergeMetadata(files: File[]): File[] {
...(file.pubMagicMetadata?.data.editedTime && { ...(file.pubMagicMetadata?.data.editedTime && {
creationTime: file.pubMagicMetadata.data.editedTime, creationTime: file.pubMagicMetadata.data.editedTime,
}), }),
...(file.pubMagicMetadata?.data.editedName && {
title: file.pubMagicMetadata.data.editedName,
}),
} }
: {}), : {}),
...(file.magicMetadata?.data ? file.magicMetadata.data : {}), ...(file.magicMetadata?.data ? file.magicMetadata.data : {}),
@ -414,3 +438,9 @@ export function updateExistingFilePubMetadata(
existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata;
existingFile.metadata = mergeMetadata([existingFile])[0].metadata; existingFile.metadata = mergeMetadata([existingFile])[0].metadata;
} }
export async function getFileFromURL(fileURL: string) {
const fileBlob = await (await fetch(fileURL)).blob();
const fileFile = new globalThis.File([fileBlob], 'temp');
return fileFile;
}

View file

@ -579,8 +579,8 @@ const englishConstants = {
SORT_BY_COLLECTION_NAME: 'album name', SORT_BY_COLLECTION_NAME: 'album name',
FIX_LARGE_THUMBNAILS: 'compress thumbnails', FIX_LARGE_THUMBNAILS: 'compress thumbnails',
THUMBNAIL_REPLACED: 'thumbnails compressed', THUMBNAIL_REPLACED: 'thumbnails compressed',
FIX: 'compress', FIX_THUMBNAIL: 'compress',
FIX_LATER: 'compress later', FIX_THUMBNAIL_LATER: 'compress later',
REPLACE_THUMBNAIL_NOT_STARTED: () => ( REPLACE_THUMBNAIL_NOT_STARTED: () => (
<> <>
some of your videos thumbnails can be compressed to save space. some of your videos thumbnails can be compressed to save space.
@ -596,6 +596,19 @@ const englishConstants = {
REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => ( REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => (
<>could not compress some of your thumbnails, please retry</> <>could not compress some of your thumbnails, please retry</>
), ),
FIX_CREATION_TIME: 'fix time',
FIX_CREATION_TIME_IN_PROGRESS: 'fixing time',
CREATION_TIME_UPDATED: `file time updated`,
UPDATE_CREATION_TIME_NOT_STARTED: () => (
<>do you want to fix time with the values found in EXIF</>
),
UPDATE_CREATION_TIME_COMPLETED: () => <>successfully updated all files</>,
UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR: () => (
<>file time updation failed for some files, please retry</>
),
FILE_NAME_CHARACTER_LIMIT: '100 characters max',
}; };
export default englishConstants; export default englishConstants;

View file

@ -1453,6 +1453,11 @@
resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g== integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/node@11.11.6":
version "11.11.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.6.tgz#df929d1bb2eee5afdda598a41930fe50b43eaa6a"
integrity sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ==
"@types/node@^14.6.4": "@types/node@^14.6.4":
version "14.17.15" version "14.17.15"
resolved "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz" resolved "https://registry.npmjs.org/@types/node/-/node-14.17.15.tgz"
@ -2085,6 +2090,16 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
bip39@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.0.4.tgz#5b11fed966840b5e1b8539f0f54ab6392969b2a0"
integrity sha512-YZKQlb752TrUWqHWj7XAwCSjYEgGAk+/Aas3V7NyjQeZYsztO8JnQUaCWhcnL4T+jL8nvB8typ2jRPzTlgugNw==
dependencies:
"@types/node" "11.11.6"
create-hash "^1.1.0"
pbkdf2 "^3.0.9"
randombytes "^2.0.1"
bluebird@^3.5.5: bluebird@^3.5.5:
version "3.7.2" version "3.7.2"
resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz"
@ -5016,7 +5031,7 @@ path-type@^4.0.0:
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pbkdf2@^3.0.3: pbkdf2@^3.0.3, pbkdf2@^3.0.9:
version "3.1.2" version "3.1.2"
resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz" resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz"
integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==