diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js index 8507713c887e..46576bc62e7a 100644 --- a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js @@ -4,7 +4,7 @@ import PropTypes from 'prop-types'; import {propTypes as anchorForAttachmentsOnlyPropTypes, defaultProps as anchorForAttachmentsOnlyDefaultProps} from './anchorForAttachmentsOnlyPropTypes'; import CONST from '../../CONST'; import ONYXKEYS from '../../ONYXKEYS'; -import AttachmentView from '../AttachmentView'; +import AttachmentView from '../Attachments/AttachmentView'; import * as Download from '../../libs/actions/Download'; import fileDownload from '../../libs/fileDownload'; import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL'; diff --git a/src/components/AttachmentCarousel/createInitialState.js b/src/components/AttachmentCarousel/createInitialState.js deleted file mode 100644 index 9c9eb7b777f9..000000000000 --- a/src/components/AttachmentCarousel/createInitialState.js +++ /dev/null @@ -1,67 +0,0 @@ -import _ from 'underscore'; -import {Parser as HtmlParser} from 'htmlparser2'; -import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; -import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; -import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot'; -import Navigation from '../../libs/Navigation/Navigation'; -import CONST from '../../CONST'; - -/** - * Constructs the initial component state from report actions - * @param {propTypes} props - * @returns {{page: Number, attachments: Array}} - */ -function createInitialState(props) { - const actions = [ReportActionsUtils.getParentReportAction(props.report), ...ReportActionsUtils.getSortedReportActions(_.values(props.reportActions))]; - const attachments = []; - - const htmlParser = new HtmlParser({ - onopentag: (name, attribs) => { - if (name !== 'img' || !attribs.src) { - return; - } - - const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]; - - // By iterating actions in chronological order and prepending each attachment - // we ensure correct order of attachments even across actions with multiple attachments. - attachments.unshift({ - source: tryResolveUrlFromApiRoot(expensifySource || attribs.src), - isAuthTokenRequired: Boolean(expensifySource), - file: {name: attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE]}, - }); - }, - }); - - _.forEach(actions, (action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) { - return; - } - htmlParser.write(_.get(action, ['message', 0, 'html'])); - }); - htmlParser.end(); - - // Inverting the list for touchscreen devices that can swipe or have an animation when scrolling - // promotes the natural feeling of swiping left/right to go to the next/previous image - // We don't want to invert the list for desktop/web because this interferes with mouse - // wheel or trackpad scrolling (in cases like document preview where you can scroll vertically) - if (DeviceCapabilities.canUseTouchScreen()) { - attachments.reverse(); - } - - const page = _.findIndex(attachments, (a) => a.source === props.source); - if (page === -1) { - Navigation.dismissModal(); - return; - } - - // Update the parent modal's state with the source and name from the mapped attachments - props.onNavigate(attachments[page]); - - return { - page, - attachments, - }; -} - -export default createInitialState; diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js deleted file mode 100644 index 3f2524a2992e..000000000000 --- a/src/components/AttachmentCarousel/index.js +++ /dev/null @@ -1,332 +0,0 @@ -import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {View, FlatList, PixelRatio, Keyboard} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import * as Expensicons from '../Icon/Expensicons'; -import styles from '../../styles/styles'; -import themeColors from '../../styles/themes/default'; -import CarouselActions from './CarouselActions'; -import Button from '../Button'; -import AttachmentView from '../AttachmentView'; -import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; -import useWindowDimensions from '../../hooks/useWindowDimensions'; -import CONST from '../../CONST'; -import ONYXKEYS from '../../ONYXKEYS'; -import Tooltip from '../Tooltip'; -import useLocalize from '../../hooks/useLocalize'; -import createInitialState from './createInitialState'; -import {propTypes, defaultProps} from './attachmentCarouselPropTypes'; - -const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); -const viewabilityConfig = { - // To facilitate paging through the attachments, we want to consider an item "viewable" when it is - // more than 90% visible. When that happens we update the page index in the state. - itemVisiblePercentThreshold: 95, -}; - -function AttachmentCarousel(props) { - const {onNavigate} = props; - - const {translate} = useLocalize(); - const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); - - const scrollRef = useRef(null); - const autoHideArrowTimeout = useRef(null); - - const [page, setPage] = useState(0); - const [attachments, setAttachments] = useState([]); - const [shouldShowArrow, setShouldShowArrow] = useState(canUseTouchScreen); - const [containerWidth, setContainerWidth] = useState(0); - const [isZoomed, setIsZoomed] = useState(false); - const [activeSource, setActiveSource] = useState(null); - - let isForwardDisabled = page === 0; - let isBackDisabled = page === _.size(attachments) - 1; - - if (canUseTouchScreen) { - isForwardDisabled = isBackDisabled; - isBackDisabled = page === 0; - } - - useEffect(() => { - const initialState = createInitialState(props); - if (initialState) { - setPage(initialState.page); - setAttachments(initialState.attachments); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.reportActions]); - - useEffect(() => { - if (!scrollRef || !scrollRef.current) { - return; - } - scrollRef.current.scrollToIndex({index: page, animated: false}); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [attachments]); - - /** - * Calculate items layout information to optimize scrolling performance - * @param {*} data - * @param {Number} index - * @returns {{offset: Number, length: Number, index: Number}} - */ - const getItemLayout = useCallback( - (data, index) => ({ - length: containerWidth, - offset: containerWidth * index, - index, - }), - [containerWidth], - ); - - /** - * Cancels the automatic hiding of the arrows. - */ - const cancelAutoHideArrow = useCallback(() => { - clearTimeout(autoHideArrowTimeout.current); - }, []); - - /** - * Toggles the visibility of the arrows - * @param {Boolean} newShouldShowArrow - */ - const toggleArrowsVisibility = useCallback( - (newShouldShowArrow) => { - // Don't toggle arrows in a zoomed state - if (isZoomed) { - return; - } - - setShouldShowArrow((prevState) => (_.isBoolean(newShouldShowArrow) ? newShouldShowArrow : !prevState)); - }, - [isZoomed], - ); - - /** - * On a touch screen device, automatically hide the arrows - * if there is no interaction for 3 seconds. - */ - const autoHideArrow = useCallback(() => { - if (!canUseTouchScreen) { - return; - } - cancelAutoHideArrow(); - autoHideArrowTimeout.current = setTimeout(() => { - toggleArrowsVisibility(false); - }, CONST.ARROW_HIDE_DELAY); - }, [cancelAutoHideArrow, toggleArrowsVisibility]); - - useEffect(() => { - if (shouldShowArrow) { - autoHideArrow(); - } else { - cancelAutoHideArrow(); - } - }, [shouldShowArrow, autoHideArrow, cancelAutoHideArrow]); - - /** - * Updates zoomed state to enable/disable panning the PDF - * @param {Number} scale current PDF scale - */ - const updateZoomState = useCallback( - (scale) => { - const newIsZoomed = scale > 1; - if (newIsZoomed === isZoomed) { - return; - } - if (newIsZoomed) { - toggleArrowsVisibility(false); - } - setIsZoomed(newIsZoomed); - }, - [isZoomed, toggleArrowsVisibility], - ); - - /** - * Increments or decrements the index to get another selected item - * @param {Number} deltaSlide - */ - const cycleThroughAttachments = useCallback( - (deltaSlide) => { - let delta = deltaSlide; - if (canUseTouchScreen) { - delta = deltaSlide * -1; - } - - const nextIndex = page - delta; - const nextItem = attachments[nextIndex]; - - if (!nextItem || !scrollRef.current) { - return; - } - - // The sliding transition is a bit too much on web, because of the wider and bigger images, - // so we only enable it for mobile - scrollRef.current.scrollToIndex({index: nextIndex, animated: canUseTouchScreen}); - }, - [attachments, page], - ); - - /** - * Updates the page state when the user navigates between attachments - * @param {Array<{item: {source, file}, index: Number}>} viewableItems - */ - const updatePage = useRef(({viewableItems}) => { - Keyboard.dismiss(); - // Since we can have only one item in view at a time, we can use the first item in the array - // to get the index of the current page - const entry = _.first(viewableItems); - if (!entry) { - setActiveSource(null); - return; - } - - const pageToSet = entry.index; - onNavigate(entry.item); - setPage(pageToSet); - setIsZoomed(false); - setActiveSource(entry.item.source); - }).current; - - /** - * Defines how a container for a single attachment should be rendered - * @param {Object} cellRendererProps - * @returns {JSX.Element} - */ - const renderCell = useCallback( - (cellRendererProps) => { - // Use window width instead of layout width to address the issue in https://github.com/Expensify/App/issues/17760 - // considering horizontal margin and border width in centered modal - const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true); - const style = [cellRendererProps.style, styles.h100, {width: PixelRatio.roundToNearestPixel(windowWidth - (modalStyles.marginHorizontal + modalStyles.borderWidth) * 2)}]; - - return ( - - ); - }, - [isSmallScreenWidth, windowWidth], - ); - - /** - * Defines how a single attachment should be rendered - * @param {Object} item - * @param {Boolean} item.isAuthTokenRequired - * @param {String} item.source - * @param {Object} item.file - * @param {String} item.file.name - * @returns {JSX.Element} - */ - const renderItem = useCallback( - ({item}) => ( - - ), - [activeSource, toggleArrowsVisibility, updateZoomState], - ); - - return ( - setContainerWidth(PixelRatio.roundToNearestPixel(nativeEvent.layout.width))} - onMouseEnter={() => !canUseTouchScreen && toggleArrowsVisibility(true)} - onMouseLeave={() => !canUseTouchScreen && toggleArrowsVisibility(false)} - > - {shouldShowArrow && ( - <> - {!isBackDisabled && ( - - -