diff --git a/package.json b/package.json index 0f589b5a6..b2ce5421a 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/parser": "^4.25.0", "axios": "^0.21.3", + "bip39": "^3.0.4", "bootstrap": "^4.5.2", "chrono-node": "^2.2.6", "comlink": "^4.3.0", diff --git a/src/components/EnteSpinner.tsx b/src/components/EnteSpinner.tsx index 35f195675..054a6c62f 100644 --- a/src/components/EnteSpinner.tsx +++ b/src/components/EnteSpinner.tsx @@ -2,16 +2,18 @@ import React from 'react'; import { Spinner } from 'react-bootstrap'; export default function EnteSpinner(props) { + const { style, ...others } = props ?? {}; return ( ); diff --git a/src/components/FixCreationTime.tsx b/src/components/FixCreationTime.tsx new file mode 100644 index 000000000..25ff329ad --- /dev/null +++ b/src/components/FixCreationTime.tsx @@ -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 ?
{message}
: <>; +} +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 ( + +
+ + + {fixState === FIX_STATE.RUNNING && ( + <> +
+ + {' '} + {progressTracker.current} /{' '} + {progressTracker.total}{' '} + {' '} + + {' '} + {constants.CREATION_TIME_UPDATED} + +
+
+ +
+ + )} + {fixState !== FIX_STATE.RUNNING && ( +
+ {(fixState === FIX_STATE.NOT_STARTED || + fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && ( + + )} + {fixState === FIX_STATE.COMPLETED && ( + + )} + {(fixState === FIX_STATE.NOT_STARTED || + fixState === FIX_STATE.COMPLETED_WITH_ERRORS) && ( + <> +
+ + + + )} +
+ )} +
+ + ); +} diff --git a/src/components/FixLargeThumbnail.tsx b/src/components/FixLargeThumbnail.tsx index bc9f6f2c2..9d574901e 100644 --- a/src/components/FixLargeThumbnail.tsx +++ b/src/components/FixLargeThumbnail.tsx @@ -189,7 +189,8 @@ export default function FixLargeThumbnails(props: Props) { display: 'flex', justifyContent: 'space-around', }}> - {fixState === FIX_STATE.NOT_STARTED ? ( + {fixState === FIX_STATE.NOT_STARTED || + fixState === FIX_STATE.FIX_LATER ? ( ) : ( )} diff --git a/src/components/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx index e688b0afd..a7f90d77a 100644 --- a/src/components/PhotoSwipe/PhotoSwipe.tsx +++ b/src/components/PhotoSwipe/PhotoSwipe.tsx @@ -10,6 +10,7 @@ import { import { ALL_TIME, File, + MAX_EDITED_FILE_NAME_LENGTH, MAX_EDITED_CREATION_TIME, MIN_EDITED_CREATION_TIME, updatePublicMagicMetadata, @@ -22,20 +23,32 @@ import styled from 'styled-components'; import events from './events'; import { changeFileCreationTime, + changeFileName, downloadFile, formatDateTime, + splitFilenameAndExtension, updateExistingFilePubMetadata, } from 'utils/file'; -import { FormCheck } from 'react-bootstrap'; +import { Col, Form, FormCheck, FormControl } from 'react-bootstrap'; import { prettyPrintExif } from 'utils/exif'; 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 DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import CloseIcon from 'components/icons/CloseIcon'; 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 { isOpen: boolean; @@ -86,7 +99,7 @@ function RenderCreationTime({ file: File; scheduleUpdate: () => void; }) { - const originalCreationTime = new Date(file.metadata.creationTime / 1000); + const originalCreationTime = new Date(file?.metadata.creationTime / 1000); const [isInEditMode, setIsInEditMode] = useState(false); const [pickedTime, setPickedTime] = useState(originalCreationTime); @@ -98,7 +111,8 @@ function RenderCreationTime({ try { if (isInEditMode && file) { const unixTimeInMicroSec = pickedTime.getTime() * 1000; - if (unixTimeInMicroSec === file.metadata.creationTime) { + if (unixTimeInMicroSec === file?.metadata.creationTime) { + closeEditMode(); return; } 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 ( + + 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 }) => ( +
+ + + + + {errors.filename} + + + {extension && ( + + + {`.${extension}`} + + + )} + + + + {loading ? ( + + ) : ( + + )} + + + + + + + +
+ )} + + ); +}; + +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 ( + <> + + + {!isInEditMode ? ( + <> + + + {getFileTitle(filename, extension)} + + + + + + + + + ) : ( + + )} + + + ); +} function ExifData(props: { exif: any }) { const { exif } = props; const [showAll, setShowAll] = useState(false); @@ -250,8 +428,12 @@ function InfoModal({ constants.FILE_ID, items[photoSwipe?.getCurrentIndex()]?.id )} - {metadata?.title && - renderInfoItem(constants.FILE_NAME, metadata.title)} + {metadata?.title && ( + + )} {metadata?.creationTime && ( ` display: flex; align-items: center; @@ -23,6 +25,7 @@ export const FreeFlowText = styled.div` word-wrap: break-word; overflow-wrap: break-word; min-width: 30%; + text-align: left; `; interface Props { show: boolean; @@ -41,7 +44,7 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) { somethingWentWrong(); props.onHide(); } - setRecoveryKey(recoveryKey); + setRecoveryKey(bip39.entropyToMnemonic(recoveryKey)); }; main(); }, [props.show]); diff --git a/src/components/icons/ClockIcon.tsx b/src/components/icons/ClockIcon.tsx new file mode 100644 index 000000000..468d165a0 --- /dev/null +++ b/src/components/icons/ClockIcon.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +export default function ClockIcon(props) { + return ( + + + + ); +} + +ClockIcon.defaultProps = { + height: 20, + width: 20, + viewBox: '0 0 24 24', +}; diff --git a/src/components/pages/gallery/SelectedFileOptions.tsx b/src/components/pages/gallery/SelectedFileOptions.tsx index 2c9276954..f10bdeefa 100644 --- a/src/components/pages/gallery/SelectedFileOptions.tsx +++ b/src/components/pages/gallery/SelectedFileOptions.tsx @@ -1,5 +1,5 @@ import { SetDialogMessage } from 'components/MessageDialog'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { SetCollectionSelectorAttributes } from './CollectionSelector'; import styled from 'styled-components'; import Navbar from 'components/Navbar'; @@ -17,6 +17,9 @@ import { OverlayTrigger } from 'react-bootstrap'; import { Collection } from 'services/collectionService'; import RemoveIcon from 'components/icons/RemoveIcon'; 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 { addToCollectionHelper: (collection: Collection) => void; @@ -27,6 +30,7 @@ interface Props { setCollectionSelectorAttributes: SetCollectionSelectorAttributes; deleteFileHelper: (permanent?: boolean) => void; removeFromCollectionHelper: () => void; + fixTimeHelper: () => void; count: number; clearSelection: () => void; archiveFilesHelper: () => void; @@ -68,6 +72,7 @@ const SelectedFileOptions = ({ restoreToCollectionHelper, showCreateCollectionModal, removeFromCollectionHelper, + fixTimeHelper, setDialogMessage, setCollectionSelectorAttributes, deleteFileHelper, @@ -78,6 +83,12 @@ const SelectedFileOptions = ({ activeCollection, isFavoriteCollection, }: 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 = () => setCollectionSelectorAttributes({ callback: addToCollectionHelper, @@ -168,6 +179,18 @@ const SelectedFileOptions = ({ ) : ( <> + {showFixCreationTime && ( + + + + + + )} + + + + + {activeCollection === ARCHIVE_SECTION && ( @@ -182,11 +205,7 @@ const SelectedFileOptions = ({ )} - - - - - + {activeCollection !== ALL_SECTION && activeCollection !== ARCHIVE_SECTION && !isFavoriteCollection && ( diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b324f1eea..8ba26963f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -446,6 +446,9 @@ const GlobalStyles = createGlobalStyle` .react-datepicker__day--disabled:hover { background-color: #202020; } + .ente-form-group{ + margin:0; + } `; export const LogoImage = styled.img` diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index e16145f72..554affbed 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -93,6 +93,9 @@ import { Trash, } from 'services/trashService'; import DeleteBtn from 'components/DeleteBtn'; +import FixCreationTime, { + FixCreationTimeAttributes, +} from 'components/FixCreationTime'; export const DeadCenter = styled.div` flex: 1; @@ -204,7 +207,9 @@ export default function Gallery() { useState>(); const [activeCollection, setActiveCollection] = useState(undefined); const [trash, setTrash] = useState([]); - + const [fixCreationTimeView, setFixCreationTimeView] = useState(false); + const [fixCreationTimeAttributes, setFixCreationTimeAttributes] = + useState(null); useEffect(() => { const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); if (!key) { @@ -243,13 +248,19 @@ export default function Gallery() { useEffect(() => setDialogView(true), [dialogMessage]); - useEffect(() => { - if (collectionSelectorAttributes) { - setCollectionSelectorView(true); - } - }, [collectionSelectorAttributes]); + useEffect( + () => collectionSelectorAttributes && setCollectionSelectorView(true), + [collectionSelectorAttributes] + ); - useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]); + useEffect( + () => collectionNamerAttributes && setCollectionNamerView(true), + [collectionNamerAttributes] + ); + useEffect( + () => fixCreationTimeAttributes && setFixCreationTimeView(true), + [fixCreationTimeAttributes] + ); useEffect(() => { 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 ( + setFixCreationTimeView(false)} + show={() => setFixCreationTimeView(true)} + attributes={fixCreationTimeAttributes} + /> { 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 masterKey: string = await cryptoWorker.decryptB64( keyAttributes.masterKeyEncryptedWithRecoveryKey, diff --git a/src/pages/two-factor/recover/index.tsx b/src/pages/two-factor/recover/index.tsx index ee0219cdd..30f599eeb 100644 --- a/src/pages/two-factor/recover/index.tsx +++ b/src/pages/two-factor/recover/index.tsx @@ -12,6 +12,9 @@ import { logError } from 'utils/sentry'; import { recoverTwoFactor, removeTwoFactor } from 'services/userService'; import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app'; import { PAGES } from 'types'; +const bip39 = require('bip39'); +// mobile client library only supports english. +bip39.setDefaultWordlist('english'); export default function Recover() { const router = useRouter(); @@ -43,6 +46,13 @@ export default function Recover() { const recover = async (recoveryKey: string, setFieldError) => { 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 twoFactorSecret: string = await cryptoWorker.decryptB64( encryptedTwoFactorSecret.encryptedData, diff --git a/src/services/ffmpegService.ts b/src/services/ffmpegService.ts index 4cfcdf9be..2b4bf2970 100644 --- a/src/services/ffmpegService.ts +++ b/src/services/ffmpegService.ts @@ -33,12 +33,17 @@ class FFmpegService { const response = this.generateThumbnailProcessor.queueUpRequest( generateThumbnailHelper.bind(null, this.ffmpeg, file) ); - - const thumbnail = await response.promise; - if (!thumbnail) { - throw Error(CustomError.THUMBNAIL_GENERATION_FAILED); + try { + return await response.promise; + } catch (e) { + if (e.message === CustomError.REQUEST_CANCELLED) { + // ignore + return null; + } else { + logError(e, 'ffmpeg thumbnail generation failed'); + throw e; + } } - return thumbnail; } } diff --git a/src/services/fileService.ts b/src/services/fileService.ts index 09570391a..0c0b2861d 100644 --- a/src/services/fileService.ts +++ b/src/services/fileService.ts @@ -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 ALL_TIME = new Date(1800, 0, 1, 23, 59, 59); +export const MAX_EDITED_FILE_NAME_LENGTH = 100; + export interface fileAttribute { encryptedData?: DataStream | Uint8Array; objectKey?: string; @@ -72,6 +74,7 @@ export interface MagicMetadata extends Omit { export interface PublicMagicMetadataProps { editedTime?: number; + editedName?: string; } export interface PublicMagicMetadata extends Omit { @@ -147,7 +150,7 @@ export const syncFiles = async ( let files = await removeDeletedCollectionFiles(collections, localFiles); if (files.length !== localFiles.length) { await setLocalFiles(files); - setFiles(sortFiles(mergeMetadata(files))); + setFiles([...sortFiles(mergeMetadata(files))]); } for (const collection of collections) { if (!getToken()) { @@ -183,9 +186,9 @@ export const syncFiles = async ( `${collection.id}-time`, collection.updationTime ); - setFiles(sortFiles(mergeMetadata(files))); + setFiles([...sortFiles(mergeMetadata(files))]); } - return mergeMetadata(files); + return sortFiles(mergeMetadata(files)); }; export const getFiles = async ( diff --git a/src/services/updateCreationTimeWithExif.ts b/src/services/updateCreationTimeWithExif.ts new file mode 100644 index 000000000..efb72c01d --- /dev/null +++ b/src/services/updateCreationTimeWithExif.ts @@ -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; +} diff --git a/src/services/upload/exifService.ts b/src/services/upload/exifService.ts index 004958b6f..8589f45ee 100644 --- a/src/services/upload/exifService.ts +++ b/src/services/upload/exifService.ts @@ -1,6 +1,8 @@ import exifr from 'exifr'; +import { logError } from 'utils/sentry'; import { NULL_LOCATION, Location } from './metadataService'; +import { FileTypeInfo } from './readFileService'; const EXIF_TAGS_NEEDED = [ 'DateTimeOriginal', @@ -17,9 +19,18 @@ interface ParsedEXIFData { } export async function getExifData( - receivedFile: globalThis.File + receivedFile: globalThis.File, + fileTypeInfo: FileTypeInfo ): Promise { - 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) { return { location: NULL_LOCATION, creationTime: null }; } diff --git a/src/services/upload/metadataService.ts b/src/services/upload/metadataService.ts index fab027ba6..305b0b202 100644 --- a/src/services/upload/metadataService.ts +++ b/src/services/upload/metadataService.ts @@ -34,14 +34,7 @@ export async function extractMetadata( ) { let exifData = null; if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { - try { - exifData = await getExifData(receivedFile); - } catch (e) { - logError(e, 'file missing exif data ', { - fileType: fileTypeInfo.exactType, - }); - // ignore exif parsing errors - } + exifData = await getExifData(receivedFile, fileTypeInfo); } const extractedMetadata: MetadataObject = { diff --git a/src/services/upload/queueProcessor.ts b/src/services/upload/queueProcessor.ts index 470659f6b..6d9e86fc6 100644 --- a/src/services/upload/queueProcessor.ts +++ b/src/services/upload/queueProcessor.ts @@ -1,6 +1,9 @@ +import { CustomError } from 'utils/common/errorUtil'; + interface RequestQueueItem { request: (canceller?: RequestCanceller) => Promise; - callback: (response) => void; + successCallback: (response: any) => void; + failureCallback: (error: Error) => void; isCanceled: { status: boolean }; canceller: { exec: () => void }; } @@ -26,10 +29,11 @@ export default class QueueProcessor { }, }; - const promise = new Promise((resolve) => { + const promise = new Promise((resolve, reject) => { this.requestQueue.push({ request, - callback: resolve, + successCallback: resolve, + failureCallback: reject, isCanceled, canceller, }); @@ -53,15 +57,15 @@ export default class QueueProcessor { let response = null; if (queueItem.isCanceled.status) { - response = null; + queueItem.failureCallback(Error(CustomError.REQUEST_CANCELLED)); } else { try { response = await queueItem.request(queueItem.canceller); + queueItem.successCallback(response); } catch (e) { - response = null; + queueItem.failureCallback(e); } } - queueItem.callback(response); } } } diff --git a/src/services/upload/thumbnailService.ts b/src/services/upload/thumbnailService.ts index f73d177d1..f4c8fd855 100644 --- a/src/services/upload/thumbnailService.ts +++ b/src/services/upload/thumbnailService.ts @@ -56,7 +56,7 @@ export async function generateThumbnail( } } catch (e) { logError(e, 'uploading static thumbnail', { - type: fileTypeInfo.exactType, + fileFormat: fileTypeInfo.exactType, }); thumbnail = Uint8Array.from(atob(BLACK_THUMBNAIL_BASE64), (c) => c.charCodeAt(0) diff --git a/src/services/userService.ts b/src/services/userService.ts index c55a9722f..f904a1e93 100644 --- a/src/services/userService.ts +++ b/src/services/userService.ts @@ -28,6 +28,8 @@ const ENDPOINT = getEndpoint(); const HAS_SET_KEYS = 'hasSetKeys'; +export const FIX_CREATION_TIME_USER_ID = 341; + export interface User { id: number; name: string; diff --git a/src/utils/common/errorUtil.ts b/src/utils/common/errorUtil.ts index 664388966..626f7fe58 100644 --- a/src/utils/common/errorUtil.ts +++ b/src/utils/common/errorUtil.ts @@ -28,6 +28,7 @@ export enum CustomError { FAV_COLLECTION_MISSING = 'favorite collection missing', INVALID_COLLECTION_OPERATION = 'invalid collection operation', WAIT_TIME_EXCEEDED = 'thumbnail generation wait time exceeded', + REQUEST_CANCELLED = 'request canceled', } function parseUploadError(error: AxiosResponse) { diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 5eaecea62..258984c94 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -240,6 +240,16 @@ export function fileExtensionWithDot(filename) { 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) { return new ReadableStream({ 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) { const user: User = getData(LS_KEYS.USER); @@ -400,6 +421,9 @@ export function mergeMetadata(files: File[]): File[] { ...(file.pubMagicMetadata?.data.editedTime && { creationTime: file.pubMagicMetadata.data.editedTime, }), + ...(file.pubMagicMetadata?.data.editedName && { + title: file.pubMagicMetadata.data.editedName, + }), } : {}), ...(file.magicMetadata?.data ? file.magicMetadata.data : {}), @@ -414,3 +438,9 @@ export function updateExistingFilePubMetadata( existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; 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; +} diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index 204afc798..69aa8d8aa 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -579,8 +579,8 @@ const englishConstants = { SORT_BY_COLLECTION_NAME: 'album name', FIX_LARGE_THUMBNAILS: 'compress thumbnails', THUMBNAIL_REPLACED: 'thumbnails compressed', - FIX: 'compress', - FIX_LATER: 'compress later', + FIX_THUMBNAIL: 'compress', + FIX_THUMBNAIL_LATER: 'compress later', REPLACE_THUMBNAIL_NOT_STARTED: () => ( <> some of your videos thumbnails can be compressed to save space. @@ -596,6 +596,19 @@ const englishConstants = { REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR: () => ( <>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; diff --git a/yarn.lock b/yarn.lock index 143b09bb9..580621f0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1453,6 +1453,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz" 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": version "14.17.15" 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" 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: version "3.7.2" 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" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -pbkdf2@^3.0.3: +pbkdf2@^3.0.3, pbkdf2@^3.0.9: version "3.1.2" resolved "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz" integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==