diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index 58618384e..ffd4bf8ad 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -1,5 +1,4 @@ import { - DeadCenter, GalleryContext, Search, SelectedState, @@ -14,18 +13,9 @@ import styled from 'styled-components'; import DownloadManager from 'services/downloadManager'; import constants from 'utils/strings/constants'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { VariableSizeList as List } from 'react-window'; import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe'; import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search'; import { SetDialogMessage } from './MessageDialog'; -import { - GAP_BTW_TILES, - DATE_CONTAINER_HEIGHT, - IMAGE_CONTAINER_MAX_HEIGHT, - IMAGE_CONTAINER_MAX_WIDTH, - MIN_COLUMNS, - SPACE_BTW_DATES, -} from 'types'; import { fileIsArchived, formatDateRelative } from 'utils/file'; import { ALL_SECTION, @@ -34,24 +24,7 @@ import { } from './pages/gallery/Collections'; import { isSharedFile } from 'utils/file'; import { isPlaybackPossible } from 'utils/photoFrame'; - -const NO_OF_PAGES = 2; -const A_DAY = 24 * 60 * 60 * 1000; - -interface TimeStampListItem { - itemType: ITEM_TYPE; - items?: File[]; - itemStartIndex?: number; - date?: string; - dates?: { - date: string; - span: number; - }[]; - groups?: number[]; - banner?: any; - id?: string; - height?: number; -} +import { PhotoList } from './PhotoList'; const Container = styled.div` display: block; @@ -66,60 +39,6 @@ const Container = styled.div` } `; -const ListItem = styled.div` - display: flex; - justify-content: center; -`; - -const getTemplateColumns = (columns: number, groups?: number[]): string => { - if (groups) { - const sum = groups.reduce((acc, item) => acc + item, 0); - if (sum < columns) { - groups[groups.length - 1] += columns - sum; - } - return groups - .map((x) => `repeat(${x}, 1fr)`) - .join(` ${SPACE_BTW_DATES}px `); - } else { - return `repeat(${columns}, 1fr)`; - } -}; - -const ListContainer = styled.div<{ columns: number; groups?: number[] }>` - user-select: none; - display: grid; - grid-template-columns: ${({ columns, groups }) => - getTemplateColumns(columns, groups)}; - grid-column-gap: ${GAP_BTW_TILES}px; - padding: 0 24px; - width: 100%; - color: #fff; - - @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) { - padding: 0 4px; - } -`; - -const DateContainer = styled.div<{ span: number }>` - user-select: none; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - grid-column: span ${(props) => props.span}; - display: flex; - align-items: center; - height: ${DATE_CONTAINER_HEIGHT}px; -`; - -const BannerContainer = styled.div<{ span: number }>` - color: #979797; - text-align: center; - grid-column: span ${(props) => props.span}; - display: flex; - justify-content: center; - align-items: flex-end; -`; - const EmptyScreen = styled.div` display: flex; justify-content: center; @@ -133,12 +52,6 @@ const EmptyScreen = styled.div` } `; -enum ITEM_TYPE { - TIME = 'TIME', - TILE = 'TILE', - BANNER = 'BANNER', -} - interface Props { files: File[]; setFiles: SetFiles; @@ -182,11 +95,11 @@ const PhotoFrame = ({ const [fetching, setFetching] = useState<{ [k: number]: boolean }>({}); const startTime = Date.now(); const galleryContext = useContext(GalleryContext); - const listRef = useRef(null); const [rangeStart, setRangeStart] = useState(null); const [currentHover, setCurrentHover] = useState(null); const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); - + const filteredDataRef = useRef([]); + const filteredData = filteredDataRef?.current ?? []; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Shift') { @@ -223,10 +136,9 @@ const PhotoFrame = ({ } }, [search]); - useEffect(() => { - listRef.current?.resetAfterIndex(0); + const resetFetching = () => { setFetching({}); - }, [files, search, deleted]); + }; useEffect(() => { if (selected.count === 0) { @@ -234,6 +146,71 @@ const PhotoFrame = ({ } }, [selected]); + useEffect(() => { + const idSet = new Set(); + filteredDataRef.current = files + .map((item, index) => ({ + ...item, + dataIndex: index, + ...(item.deleteBy && { + title: constants.AUTOMATIC_BIN_DELETE_MESSAGE( + formatDateRelative(item.deleteBy / 1000) + ), + }), + })) + .filter((item) => { + if (deleted.includes(item.id)) { + return false; + } + if ( + search.date && + !isSameDayAnyYear(search.date)( + new Date(item.metadata.creationTime / 1000) + ) + ) { + return false; + } + if ( + search.location && + !isInsideBox(item.metadata, search.location) + ) { + return false; + } + if (activeCollection === ALL_SECTION && fileIsArchived(item)) { + return false; + } + if ( + activeCollection === ARCHIVE_SECTION && + !fileIsArchived(item) + ) { + return false; + } + + if (isSharedFile(item) && !isSharedCollection) { + return false; + } + if (activeCollection === TRASH_SECTION && !item.isTrashed) { + return false; + } + if (activeCollection !== TRASH_SECTION && item.isTrashed) { + return false; + } + if (!idSet.has(item.id)) { + if ( + activeCollection === ALL_SECTION || + activeCollection === ARCHIVE_SECTION || + activeCollection === TRASH_SECTION || + activeCollection === item.collectionID + ) { + idSet.add(item.id); + return true; + } + return false; + } + return false; + }); + }, [files, deleted, search, activeCollection]); + const updateUrl = (index: number) => (url: string) => { files[index] = { ...files[index], @@ -310,9 +287,7 @@ const PhotoFrame = ({ if (selected.collectionID !== activeCollection) { setSelected({ count: 0, collectionID: 0 }); } - if (rangeStart || rangeStart === 0) { - setRangeStart(null); - } else if (checked) { + if (checked) { setRangeStart(index); } @@ -332,11 +307,11 @@ const PhotoFrame = ({ let leftEnd = -1; let rightEnd = -1; if (index < rangeStart) { - leftEnd = index; - rightEnd = rangeStart; + leftEnd = index + 1; + rightEnd = rangeStart - 1; } else { - leftEnd = rangeStart; - rightEnd = index; + leftEnd = rangeStart + 1; + rightEnd = index - 1; } for (let i = leftEnd; i <= rightEnd; i++) { handleSelect(filteredData[i].id)(true); @@ -364,8 +339,8 @@ const PhotoFrame = ({ isShiftKeyPressed && (rangeStart || rangeStart === 0) } isInsSelectRange={ - (index >= rangeStart + 1 && index <= currentHover) || - (index >= currentHover && index <= rangeStart - 1) + (index >= rangeStart && index <= currentHover) || + (index >= currentHover && index <= rangeStart) } /> ); @@ -426,149 +401,6 @@ const PhotoFrame = ({ } }; - const idSet = new Set(); - const filteredData = files - .map((item, index) => ({ - ...item, - dataIndex: index, - ...(item.deleteBy && { - title: constants.AUTOMATIC_BIN_DELETE_MESSAGE( - formatDateRelative(item.deleteBy / 1000) - ), - }), - })) - .filter((item) => { - if (deleted.includes(item.id)) { - return false; - } - if ( - search.date && - !isSameDayAnyYear(search.date)( - new Date(item.metadata.creationTime / 1000) - ) - ) { - return false; - } - if ( - search.location && - !isInsideBox(item.metadata, search.location) - ) { - return false; - } - if (activeCollection === ALL_SECTION && fileIsArchived(item)) { - return false; - } - if (activeCollection === ARCHIVE_SECTION && !fileIsArchived(item)) { - return false; - } - - if (isSharedFile(item) && !isSharedCollection) { - return false; - } - if (activeCollection === TRASH_SECTION && !item.isTrashed) { - return false; - } - if (activeCollection !== TRASH_SECTION && item.isTrashed) { - return false; - } - if (!idSet.has(item.id)) { - if ( - activeCollection === ALL_SECTION || - activeCollection === ARCHIVE_SECTION || - activeCollection === TRASH_SECTION || - activeCollection === item.collectionID - ) { - idSet.add(item.id); - return true; - } - return false; - } - return false; - }); - - const isSameDay = (first, second) => - first.getFullYear() === second.getFullYear() && - first.getMonth() === second.getMonth() && - first.getDate() === second.getDate(); - - /** - * Checks and merge multiple dates into a single row. - * - * @param items - * @param columns - * @returns - */ - const mergeTimeStampList = ( - items: TimeStampListItem[], - columns: number - ): TimeStampListItem[] => { - const newList: TimeStampListItem[] = []; - let index = 0; - let newIndex = 0; - while (index < items.length) { - const currItem = items[index]; - // If the current item is of type time, then it is not part of an ongoing date. - // So, there is a possibility of merge. - if (currItem.itemType === ITEM_TYPE.TIME) { - // If new list pointer is not at the end of list then - // we can add more items to the same list. - if (newList[newIndex]) { - // Check if items can be added to same list - if ( - newList[newIndex + 1].items.length + - items[index + 1].items.length <= - columns - ) { - newList[newIndex].dates.push({ - date: currItem.date, - span: items[index + 1].items.length, - }); - newList[newIndex + 1].items = newList[ - newIndex + 1 - ].items.concat(items[index + 1].items); - index += 2; - } else { - // Adding items would exceed the number of columns. - // So, move new list pointer to the end. Hence, in next iteration, - // items will be added to a new list. - newIndex += 2; - } - } else { - // New list pointer was at the end of list so simply add new items to the list. - newList.push({ - ...currItem, - date: null, - dates: [ - { - date: currItem.date, - span: items[index + 1].items.length, - }, - ], - }); - newList.push(items[index + 1]); - index += 2; - } - } else { - // Merge cannot happen. Simply add all items to new list - // and set new list point to the end of list. - newList.push(currItem); - index++; - newIndex = newList.length; - } - } - for (let i = 0; i < newList.length; i++) { - const currItem = newList[i]; - const nextItem = newList[i + 1]; - if (currItem.itemType === ITEM_TYPE.TIME) { - if (currItem.dates.length > 1) { - currItem.groups = currItem.dates.map((item) => item.span); - nextItem.groups = currItem.groups; - } - } - } - return newList; - }; - return ( <> {!isFirstLoad && files.length === 0 && !isInSearchMode ? ( @@ -591,217 +423,22 @@ const PhotoFrame = ({ {constants.UPLOAD_FIRST_PHOTO} - ) : filteredData.length ? ( + ) : ( - {({ height, width }) => { - let columns = Math.floor( - width / IMAGE_CONTAINER_MAX_WIDTH - ); - let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT; - let skipMerge = false; - if (columns < MIN_COLUMNS) { - columns = MIN_COLUMNS; - listItemHeight = width / MIN_COLUMNS; - skipMerge = true; - } - - let timeStampList: TimeStampListItem[] = []; - let listItemIndex = 0; - let currentDate = -1; - filteredData.forEach((item, index) => { - if ( - !isSameDay( - new Date( - item.metadata.creationTime / 1000 - ), - new Date(currentDate) - ) - ) { - currentDate = - item.metadata.creationTime / 1000; - const dateTimeFormat = - new Intl.DateTimeFormat('en-IN', { - weekday: 'short', - year: 'numeric', - month: 'short', - day: 'numeric', - }); - timeStampList.push({ - itemType: ITEM_TYPE.TIME, - date: isSameDay( - new Date(currentDate), - new Date() - ) - ? 'Today' - : isSameDay( - new Date(currentDate), - new Date(Date.now() - A_DAY) - ) - ? 'Yesterday' - : dateTimeFormat.format( - currentDate - ), - id: currentDate.toString(), - }); - timeStampList.push({ - itemType: ITEM_TYPE.TILE, - items: [item], - itemStartIndex: index, - }); - listItemIndex = 1; - } else if (listItemIndex < columns) { - timeStampList[ - timeStampList.length - 1 - ].items.push(item); - listItemIndex++; - } else { - listItemIndex = 1; - timeStampList.push({ - itemType: ITEM_TYPE.TILE, - items: [item], - itemStartIndex: index, - }); + {({ height, width }) => ( + { - switch (timeStampList[index].itemType) { - case ITEM_TYPE.TIME: - return DATE_CONTAINER_HEIGHT; - case ITEM_TYPE.TILE: - return listItemHeight; - default: - return timeStampList[index].height; - } - }; - - const photoFrameHeight = (() => { - let sum = 0; - for (let i = 0; i < timeStampList.length; i++) { - sum += getItemSize(i); - } - return sum; - })(); - files.length < 30 && - !isInSearchMode && - timeStampList.push({ - itemType: ITEM_TYPE.BANNER, - banner: ( - -

- {constants.INSTALL_MOBILE_APP()} -

-
- ), - id: 'install-banner', - height: Math.max( - 48, - height - photoFrameHeight - ), - }); - const extraRowsToRender = Math.ceil( - (NO_OF_PAGES * height) / - IMAGE_CONTAINER_MAX_HEIGHT - ); - - const generateKey = (index) => { - switch (timeStampList[index].itemType) { - case ITEM_TYPE.TILE: - return `${ - timeStampList[index].items[0].id - }-${ - timeStampList[index].items.slice( - -1 - )[0].id - }`; - default: - return `${timeStampList[index].id}-${index}`; - } - }; - - const renderListItem = ( - listItem: TimeStampListItem - ) => { - switch (listItem.itemType) { - case ITEM_TYPE.TIME: - return listItem.dates ? ( - listItem.dates.map((item) => ( - <> - - {item.date} - -
- - )) - ) : ( - - {listItem.date} - - ); - case ITEM_TYPE.BANNER: - return listItem.banner; - default: { - const ret = listItem.items.map( - (item, idx) => - getThumbnail( - filteredData, - listItem.itemStartIndex + - idx - ) - ); - if (listItem.groups) { - let sum = 0; - for ( - let i = 0; - i < listItem.groups.length - 1; - i++ - ) { - sum = sum + listItem.groups[i]; - ret.splice(sum, 0,
); - sum += 1; - } - } - return ret; - } - } - }; - - return ( - - {({ index, style }) => ( - - - {renderListItem( - timeStampList[index] - )} - - - )} - - ); - }} + resetFetching={resetFetching} + /> + )} - ) : ( - -
{constants.NOTHING_HERE}
-
)} ); diff --git a/src/components/PhotoList.tsx b/src/components/PhotoList.tsx new file mode 100644 index 000000000..f8dce46ab --- /dev/null +++ b/src/components/PhotoList.tsx @@ -0,0 +1,409 @@ +import React, { useRef, useEffect } from 'react'; +import { VariableSizeList as List } from 'react-window'; +import styled from 'styled-components'; +import { File } from 'services/fileService'; +import { + IMAGE_CONTAINER_MAX_WIDTH, + IMAGE_CONTAINER_MAX_HEIGHT, + MIN_COLUMNS, + DATE_CONTAINER_HEIGHT, + GAP_BTW_TILES, + SPACE_BTW_DATES, +} from 'types'; +import constants from 'utils/strings/constants'; + +const A_DAY = 24 * 60 * 60 * 1000; +const NO_OF_PAGES = 2; + +enum ITEM_TYPE { + TIME = 'TIME', + TILE = 'TILE', + BANNER = 'BANNER', +} + +interface TimeStampListItem { + itemType: ITEM_TYPE; + items?: File[]; + itemStartIndex?: number; + date?: string; + dates?: { + date: string; + span: number; + }[]; + groups?: number[]; + banner?: any; + id?: string; + height?: number; +} + +const ListItem = styled.div` + display: flex; + justify-content: center; +`; + +const getTemplateColumns = (columns: number, groups?: number[]): string => { + if (groups) { + const sum = groups.reduce((acc, item) => acc + item, 0); + if (sum < columns) { + groups[groups.length - 1] += columns - sum; + } + return groups + .map((x) => `repeat(${x}, 1fr)`) + .join(` ${SPACE_BTW_DATES}px `); + } else { + return `repeat(${columns}, 1fr)`; + } +}; + +const ListContainer = styled.div<{ columns: number; groups?: number[] }>` + user-select: none; + display: grid; + grid-template-columns: ${({ columns, groups }) => + getTemplateColumns(columns, groups)}; + grid-column-gap: ${GAP_BTW_TILES}px; + padding: 0 24px; + width: 100%; + color: #fff; + + @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) { + padding: 0 4px; + } +`; + +const DateContainer = styled.div<{ span: number }>` + user-select: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + grid-column: span ${(props) => props.span}; + display: flex; + align-items: center; + height: ${DATE_CONTAINER_HEIGHT}px; +`; + +const BannerContainer = styled.div<{ span: number }>` + color: #979797; + text-align: center; + grid-column: span ${(props) => props.span}; + display: flex; + justify-content: center; + align-items: flex-end; +`; + +const NothingContainer = styled.div<{ span: number }>` + color: #979797; + text-align: center; + grid-column: span ${(props) => props.span}; + display: flex; + justify-content: center; + align-items: center; +`; + +interface Props { + height: number; + width: number; + filteredData: File[]; + showBanner: boolean; + getThumbnail: (file: File[], index: number) => JSX.Element; + activeCollection: number; + resetFetching: () => void; +} + +export function PhotoList({ + height, + width, + filteredData, + showBanner, + getThumbnail, + activeCollection, + resetFetching, +}: Props) { + const timeStampListRef = useRef([]); + const timeStampList = timeStampListRef?.current ?? []; + const filteredDataCopyRef = useRef([]); + const filteredDataCopy = filteredDataCopyRef.current ?? []; + const listRef = useRef(null); + + let columns = Math.floor(width / IMAGE_CONTAINER_MAX_WIDTH); + let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT; + + let skipMerge = false; + if (columns < MIN_COLUMNS) { + columns = MIN_COLUMNS; + listItemHeight = width / MIN_COLUMNS; + skipMerge = true; + } + + const refreshList = () => { + listRef.current?.resetAfterIndex(0); + resetFetching(); + }; + + useEffect(() => { + let timeStampList: TimeStampListItem[] = []; + let listItemIndex = 0; + let currentDate = -1; + + filteredData.forEach((item, index) => { + if ( + !isSameDay( + new Date(item.metadata.creationTime / 1000), + new Date(currentDate) + ) + ) { + currentDate = item.metadata.creationTime / 1000; + const dateTimeFormat = new Intl.DateTimeFormat('en-IN', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + }); + timeStampList.push({ + itemType: ITEM_TYPE.TIME, + date: isSameDay(new Date(currentDate), new Date()) + ? 'Today' + : isSameDay( + new Date(currentDate), + new Date(Date.now() - A_DAY) + ) + ? 'Yesterday' + : dateTimeFormat.format(currentDate), + id: currentDate.toString(), + }); + timeStampList.push({ + itemType: ITEM_TYPE.TILE, + items: [item], + itemStartIndex: index, + }); + listItemIndex = 1; + } else if (listItemIndex < columns) { + timeStampList[timeStampList.length - 1].items.push(item); + listItemIndex++; + } else { + listItemIndex = 1; + timeStampList.push({ + itemType: ITEM_TYPE.TILE, + items: [item], + itemStartIndex: index, + }); + } + }); + + if (!skipMerge) { + timeStampList = mergeTimeStampList(timeStampList, columns); + } + if (timeStampList.length === 0) { + timeStampList.push(getEmptyListItem()); + } + if (showBanner) { + timeStampList.push(getBannerItem(timeStampList)); + } + + timeStampListRef.current = timeStampList; + filteredDataCopyRef.current = filteredData; + refreshList(); + }, [width, height, filteredData, showBanner]); + + const isSameDay = (first, second) => + first.getFullYear() === second.getFullYear() && + first.getMonth() === second.getMonth() && + first.getDate() === second.getDate(); + + const getEmptyListItem = () => { + return { + itemType: ITEM_TYPE.BANNER, + banner: ( + +
{constants.NOTHING_HERE}
+
+ ), + id: 'empty-list-banner', + height: height - 48, + }; + }; + + const getBannerItem = (timeStampList) => { + const photoFrameHeight = (() => { + let sum = 0; + const getCurrentItemSize = getItemSize(timeStampList); + for (let i = 0; i < timeStampList.length; i++) { + sum += getCurrentItemSize(i); + } + return sum; + })(); + return { + itemType: ITEM_TYPE.BANNER, + banner: ( + +

{constants.INSTALL_MOBILE_APP()}

+
+ ), + id: 'install-banner', + height: Math.max(48, height - photoFrameHeight), + }; + }; + /** + * Checks and merge multiple dates into a single row. + * + * @param items + * @param columns + * @returns + */ + const mergeTimeStampList = ( + items: TimeStampListItem[], + columns: number + ): TimeStampListItem[] => { + const newList: TimeStampListItem[] = []; + let index = 0; + let newIndex = 0; + while (index < items.length) { + const currItem = items[index]; + // If the current item is of type time, then it is not part of an ongoing date. + // So, there is a possibility of merge. + if (currItem.itemType === ITEM_TYPE.TIME) { + // If new list pointer is not at the end of list then + // we can add more items to the same list. + if (newList[newIndex]) { + // Check if items can be added to same list + if ( + newList[newIndex + 1].items.length + + items[index + 1].items.length <= + columns + ) { + newList[newIndex].dates.push({ + date: currItem.date, + span: items[index + 1].items.length, + }); + newList[newIndex + 1].items = newList[ + newIndex + 1 + ].items.concat(items[index + 1].items); + index += 2; + } else { + // Adding items would exceed the number of columns. + // So, move new list pointer to the end. Hence, in next iteration, + // items will be added to a new list. + newIndex += 2; + } + } else { + // New list pointer was at the end of list so simply add new items to the list. + newList.push({ + ...currItem, + date: null, + dates: [ + { + date: currItem.date, + span: items[index + 1].items.length, + }, + ], + }); + newList.push(items[index + 1]); + index += 2; + } + } else { + // Merge cannot happen. Simply add all items to new list + // and set new list point to the end of list. + newList.push(currItem); + index++; + newIndex = newList.length; + } + } + for (let i = 0; i < newList.length; i++) { + const currItem = newList[i]; + const nextItem = newList[i + 1]; + if (currItem.itemType === ITEM_TYPE.TIME) { + if (currItem.dates.length > 1) { + currItem.groups = currItem.dates.map((item) => item.span); + nextItem.groups = currItem.groups; + } + } + } + return newList; + }; + + const getItemSize = (timeStampList) => (index) => { + switch (timeStampList[index].itemType) { + case ITEM_TYPE.TIME: + return DATE_CONTAINER_HEIGHT; + case ITEM_TYPE.TILE: + return listItemHeight; + default: + return timeStampList[index].height; + } + }; + + const extraRowsToRender = Math.ceil( + (NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT + ); + + const generateKey = (index) => { + switch (timeStampList[index].itemType) { + case ITEM_TYPE.TILE: + return `${timeStampList[index].items[0].id}-${ + timeStampList[index].items.slice(-1)[0].id + }`; + default: + return `${timeStampList[index].id}-${index}`; + } + }; + + const renderListItem = (listItem: TimeStampListItem) => { + switch (listItem.itemType) { + case ITEM_TYPE.TIME: + return listItem.dates ? ( + listItem.dates.map((item) => ( + <> + + {item.date} + +
+ + )) + ) : ( + + {listItem.date} + + ); + case ITEM_TYPE.BANNER: + return listItem.banner; + default: { + const ret = listItem.items.map((item, idx) => + getThumbnail( + filteredDataCopy, + listItem.itemStartIndex + idx + ) + ); + if (listItem.groups) { + let sum = 0; + for (let i = 0; i < listItem.groups.length - 1; i++) { + sum = sum + listItem.groups[i]; + ret.splice(sum, 0,
); + sum += 1; + } + } + return ret; + } + } + }; + + return ( + + {({ index, style }) => ( + + + {renderListItem(timeStampList[index])} + + + )} + + ); +} diff --git a/src/components/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx index 27c7e495f..e7e39c2b0 100644 --- a/src/components/PhotoSwipe/PhotoSwipe.tsx +++ b/src/components/PhotoSwipe/PhotoSwipe.tsx @@ -8,8 +8,10 @@ import { removeFromFavorites, } from 'services/collectionService'; import { + ALL_TIME, File, MAX_EDITED_FILE_NAME_LENGTH, + MAX_EDITED_CREATION_TIME, MIN_EDITED_CREATION_TIME, updatePublicMagicMetadata, } from 'services/fileService'; @@ -40,8 +42,8 @@ import { logError } from 'utils/sentry'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; -import TickIcon from 'components/icons/TickIcon'; import CloseIcon from 'components/icons/CloseIcon'; +import TickIcon from 'components/icons/TickIcon'; interface Iprops { isOpen: boolean; @@ -73,11 +75,6 @@ const Pre = styled.pre` padding: 7px 15px; `; -const ButtonContainer = styled.div` - margin-left: auto; - width: 200px; - padding: 5px 10px; -`; const WarningMessage = styled.div` width: 100%; margin-top: 0.25rem; @@ -92,6 +89,11 @@ const renderInfoItem = (label: string, value: string | JSX.Element) => ( ); +const isSameDay = (first, second) => + first.getFullYear() === second.getFullYear() && + first.getMonth() === second.getMonth() && + first.getDate() === second.getDate(); + function RenderCreationTime({ file, scheduleUpdate, @@ -106,6 +108,7 @@ function RenderCreationTime({ const openEditMode = () => setIsInEditMode(true); const closeEditMode = () => setIsInEditMode(false); + const saveEdits = async () => { try { if (isInEditMode && file) { @@ -150,35 +153,18 @@ function RenderCreationTime({ onChange={handleChange} timeInputLabel="Time:" dateFormat="dd/MM/yyyy h:mm aa" - showTimeInput + showTimeSelect autoFocus - shouldCloseOnSelect={false} - onClickOutside={discardEdits} - minDate={new Date(MIN_EDITED_CREATION_TIME)} - maxDate={new Date()} - showYearDropdown - showMonthDropdown - withPortal> - - - - - + minDate={MIN_EDITED_CREATION_TIME} + maxDate={MAX_EDITED_CREATION_TIME} + maxTime={ + isSameDay(pickedTime, new Date()) + ? MAX_EDITED_CREATION_TIME + : ALL_TIME + } + minTime={MIN_EDITED_CREATION_TIME} + fixedHeight + withPortal> ) : ( formatDateTime(pickedTime) )} @@ -186,9 +172,20 @@ function RenderCreationTime({ - - - + {!isInEditMode ? ( + + + + ) : ( + <> + + + + + + + + )} diff --git a/src/components/icons/TickIcon.tsx b/src/components/icons/TickIcon.tsx index 8bd76968f..633d869a2 100644 --- a/src/components/icons/TickIcon.tsx +++ b/src/components/icons/TickIcon.tsx @@ -14,7 +14,7 @@ export default function TickIcon(props) { } TickIcon.defaultProps = { - height: 28, + height: 20, width: 20, viewBox: '0 0 24 24', }; diff --git a/src/components/pages/gallery/CollectionSortOptions.tsx b/src/components/pages/gallery/CollectionSortOptions.tsx index 73241c145..aff226140 100644 --- a/src/components/pages/gallery/CollectionSortOptions.tsx +++ b/src/components/pages/gallery/CollectionSortOptions.tsx @@ -1,4 +1,4 @@ -import { Label, Value } from 'components/Container'; +import { Value } from 'components/Container'; import TickIcon from 'components/icons/TickIcon'; import React from 'react'; import { ListGroup, Popover, Row } from 'react-bootstrap'; @@ -23,13 +23,13 @@ const SortByOptionCreator = ( - + setCollectionSortBy(props.sortBy)} diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx index f3dcd4a31..b04963826 100644 --- a/src/components/pages/gallery/PreviewCard.tsx +++ b/src/components/pages/gallery/PreviewCard.tsx @@ -217,9 +217,8 @@ export default function PreviewCard(props: IProps) { if (selectOnClick) { if (isRangeSelectActive) { onRangeSelect(); - } else { - onSelect?.(!selected); } + onSelect?.(!selected); } else if (file?.msrc || imgSrc) { onClick?.(); } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 332e30bac..b324f1eea 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -413,6 +413,39 @@ const GlobalStyles = createGlobalStyle` .react-datepicker__input-container > input { width:100%; } + .react-datepicker__navigation{ + top:14px; + } + .react-datepicker, .react-datepicker__header,.react-datepicker__time-container .react-datepicker__time,.react-datepicker-time__header{ + background-color: #202020; + color:#fff; + border-color: #444; + } + .react-datepicker__current-month,.react-datepicker__day-name, .react-datepicker__day, .react-datepicker__time-name{ + color:#fff; + } + .react-datepicker__day--disabled{ + color:#5b5656; + } + .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{ + background-color:#686868 + } + .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled :hover{ + background-color: #202020; + } + + .react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled{ + color:#5b5656; + } + .react-datepicker{ + padding-bottom:10px; + } + .react-datepicker__day:hover { + background-color:#686868 + } + .react-datepicker__day--disabled:hover { + background-color: #202020; + } `; export const LogoImage = styled.img` diff --git a/src/services/fileService.ts b/src/services/fileService.ts index 522f61a8c..c6add81a3 100644 --- a/src/services/fileService.ts +++ b/src/services/fileService.ts @@ -10,19 +10,16 @@ import { import { Collection } from './collectionService'; import HTTPService from './HTTPService'; import { logError } from 'utils/sentry'; -import { - appendPhotoSwipeProps, - decryptFile, - mergeMetadata, - sortFiles, -} from 'utils/file'; +import { decryptFile, mergeMetadata, sortFiles } from 'utils/file'; import CryptoWorker from 'utils/crypto'; const ENDPOINT = getEndpoint(); -const FILES = 'files'; +const FILES_TABLE = 'files'; -export const MIN_EDITED_CREATION_TIME = '1800-01-01T00:00:00.000Z'; +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; @@ -133,10 +130,18 @@ interface TrashRequestItems { collectionID: number; } export const getLocalFiles = async () => { - const files: Array = (await localForage.getItem(FILES)) || []; + const files: Array = + (await localForage.getItem(FILES_TABLE)) || []; return files; }; +export const setLocalFiles = async (files: File[]) => { + await localForage.setItem(FILES_TABLE, files); +}; + +const getCollectionLastSyncTime = async (collection: Collection) => + (await localForage.getItem(`${collection.id}-time`)) ?? 0; + export const syncFiles = async ( collections: Collection[], setFiles: (files: File[]) => void @@ -144,15 +149,14 @@ export const syncFiles = async ( const localFiles = await getLocalFiles(); let files = await removeDeletedCollectionFiles(collections, localFiles); if (files.length !== localFiles.length) { - await localForage.setItem('files', files); + await setLocalFiles(files); setFiles(sortFiles(mergeMetadata(files))); } for (const collection of collections) { if (!getToken()) { continue; } - const lastSyncTime = - (await localForage.getItem(`${collection.id}-time`)) ?? 0; + const lastSyncTime = await getCollectionLastSyncTime(collection); if (collection.updationTime === lastSyncTime) { continue; } @@ -177,15 +181,14 @@ export const syncFiles = async ( } files.push(file); } - await localForage.setItem('files', files); + await setLocalFiles(files); await localForage.setItem( `${collection.id}-time`, collection.updationTime ); - files = sortFiles(mergeMetadata(appendPhotoSwipeProps(files))); - setFiles(files); + setFiles(sortFiles(mergeMetadata(files))); } - return mergeMetadata(appendPhotoSwipeProps(files)); + return mergeMetadata(files); }; export const getFiles = async ( @@ -196,10 +199,7 @@ export const getFiles = async ( ): Promise => { try { const decryptedFiles: File[] = []; - let time = - sinceTime || - (await localForage.getItem(`${collection.id}-time`)) || - 0; + let time = sinceTime; let resp; do { const token = getToken(); diff --git a/src/services/migrateThumbnailService.ts b/src/services/migrateThumbnailService.ts index a6a16e349..2eb769707 100644 --- a/src/services/migrateThumbnailService.ts +++ b/src/services/migrateThumbnailService.ts @@ -10,6 +10,7 @@ import uploadHttpClient from 'services/upload/uploadHttpClient'; import { EncryptionResult, UploadURL } from 'services/upload/uploadService'; import { SetProgressTracker } from 'components/FixLargeThumbnail'; import { getFileType } from './upload/readFileService'; +import { getLocalTrash, getTrashedFiles } from './trashService'; const ENDPOINT = getEndpoint(); const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB @@ -43,10 +44,14 @@ export async function replaceThumbnail( const token = getToken(); const worker = await new CryptoWorker(); const files = await getLocalFiles(); - const largeThumbnailFiles = files.filter((file) => + const trash = await getLocalTrash(); + const trashFiles = getTrashedFiles(trash); + const largeThumbnailFiles = [...files, ...trashFiles].filter((file) => largeThumbnailFileIDs.has(file.id) ); - + if (largeThumbnailFileIDs.size !== largeThumbnailFiles.length) { + logError(Error(), 'all large thumbnail files not found locally'); + } if (largeThumbnailFiles.length === 0) { return completedWithError; } diff --git a/src/services/trashService.ts b/src/services/trashService.ts index 882d6abc1..11cc3933a 100644 --- a/src/services/trashService.ts +++ b/src/services/trashService.ts @@ -1,12 +1,7 @@ import { SetFiles } from 'pages/gallery'; import { getEndpoint } from 'utils/common/apiUtil'; import { getToken } from 'utils/common/key'; -import { - appendPhotoSwipeProps, - decryptFile, - mergeMetadata, - sortFiles, -} from 'utils/file'; +import { decryptFile, mergeMetadata, sortFiles } from 'utils/file'; import { logError } from 'utils/sentry'; import localForage from 'utils/storage/localForage'; import { Collection, getCollection } from './collectionService'; @@ -167,14 +162,12 @@ function removeRestoredOrDeletedTrashItems(trash: Trash) { export function getTrashedFiles(trash: Trash) { return mergeMetadata( - appendPhotoSwipeProps( - trash.map((trashedFile) => ({ - ...trashedFile.file, - updationTime: trashedFile.updatedAt, - isTrashed: true, - deleteBy: trashedFile.deleteBy, - })) - ) + trash.map((trashedFile) => ({ + ...trashedFile.file, + updationTime: trashedFile.updatedAt, + isTrashed: true, + deleteBy: trashedFile.deleteBy, + })) ); } diff --git a/src/services/upload/exifService.ts b/src/services/upload/exifService.ts index cacd81f2c..004958b6f 100644 --- a/src/services/upload/exifService.ts +++ b/src/services/upload/exifService.ts @@ -1,6 +1,5 @@ import exifr from 'exifr'; -import { logError } from 'utils/sentry'; import { NULL_LOCATION, Location } from './metadataService'; const EXIF_TAGS_NEEDED = [ @@ -20,20 +19,15 @@ interface ParsedEXIFData { export async function getExifData( receivedFile: globalThis.File ): Promise { - try { - const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED); - if (!exifData) { - return { location: NULL_LOCATION, creationTime: null }; - } - const parsedEXIFData = { - location: getEXIFLocation(exifData), - creationTime: getUNIXTime(exifData), - }; - return parsedEXIFData; - } catch (e) { - logError(e, 'error reading exif data'); - // ignore exif parsing errors + const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED); + if (!exifData) { + return { location: NULL_LOCATION, creationTime: null }; } + const parsedEXIFData = { + location: getEXIFLocation(exifData), + creationTime: getUNIXTime(exifData), + }; + return parsedEXIFData; } function getUNIXTime(exifData: any) { diff --git a/src/services/upload/metadataService.ts b/src/services/upload/metadataService.ts index 530f77084..fab027ba6 100644 --- a/src/services/upload/metadataService.ts +++ b/src/services/upload/metadataService.ts @@ -34,7 +34,14 @@ export async function extractMetadata( ) { let exifData = null; if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) { - exifData = await getExifData(receivedFile); + try { + exifData = await getExifData(receivedFile); + } catch (e) { + logError(e, 'file missing exif data ', { + fileType: fileTypeInfo.exactType, + }); + // ignore exif parsing errors + } } const extractedMetadata: MetadataObject = { diff --git a/src/services/upload/uploadManager.ts b/src/services/upload/uploadManager.ts index 7cb91377b..1c67cbb6d 100644 --- a/src/services/upload/uploadManager.ts +++ b/src/services/upload/uploadManager.ts @@ -1,4 +1,4 @@ -import { File, getLocalFiles } from '../fileService'; +import { File, getLocalFiles, setLocalFiles } from '../fileService'; import { Collection, getLocalCollections } from '../collectionService'; import { SetFiles } from 'pages/gallery'; import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto'; @@ -8,7 +8,6 @@ import { removeUnnecessaryFileProps, } from 'utils/file'; import { logError } from 'utils/sentry'; -import localForage from 'utils/storage/localForage'; import { getMetadataMapKey, ParsedMetaDataJSON, @@ -185,8 +184,7 @@ class UploadManager { if (fileUploadResult === FileUploadResults.UPLOADED) { this.existingFiles.push(file); this.existingFiles = sortFiles(this.existingFiles); - await localForage.setItem( - 'files', + await setLocalFiles( removeUnnecessaryFileProps(this.existingFiles) ); this.setFiles(this.existingFiles); diff --git a/src/utils/file/index.ts b/src/utils/file/index.ts index 9efd84447..221fda554 100644 --- a/src/utils/file/index.ts +++ b/src/utils/file/index.ts @@ -420,13 +420,6 @@ export function mergeMetadata(files: File[]): File[] { }, })); } -export function appendPhotoSwipeProps(files: File[]) { - return files.map((file) => ({ - ...file, - w: window.innerWidth, - h: window.innerHeight, - })) as File[]; -} export function updateExistingFilePubMetadata( existingFile: File,