diff --git a/src/CONST.js b/src/CONST.js
index 7ef61b965e42..80f8ed3ccd59 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -823,6 +823,10 @@ const CONST = {
EMOJIS: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu,
TAX_ID: /^\d{9}$/,
NON_NUMERIC: /\D/g,
+
+ // Extract attachment's source from the data's html string
+ ATTACHMENT_DATA: /(data-expensify-source|data-name)="([^"]+)"/g,
+
NON_NUMERIC_WITH_PLUS: /[^0-9+]/g,
EMOJI_NAME: /:[\w+-]+:/g,
EMOJI_SUGGESTIONS: /:[a-zA-Z0-9_+-]{1,40}$/,
diff --git a/src/components/AttachmentCarousel/CarouselActions/index.js b/src/components/AttachmentCarousel/CarouselActions/index.js
new file mode 100644
index 000000000000..26af8917a04a
--- /dev/null
+++ b/src/components/AttachmentCarousel/CarouselActions/index.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {Pressable} from 'react-native';
+
+const propTypes = {
+ /** Handles onPress events with a callback */
+ onPress: PropTypes.func.isRequired,
+
+ /** Callback to cycle through attachments */
+ onCycleThroughAttachments: PropTypes.func.isRequired,
+
+ /** Styles to be assigned to Carousel */
+ styles: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
+
+ /** Children to render */
+ children: PropTypes.oneOfType([
+ PropTypes.func,
+ PropTypes.node,
+ ]).isRequired,
+};
+
+class Carousel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleKeyPress = this.handleKeyPress.bind(this);
+ }
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleKeyPress);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleKeyPress);
+ }
+
+ /**
+ * Listens for keyboard shortcuts and applies the action
+ *
+ * @param {Object} e
+ */
+ handleKeyPress(e) {
+ // prevents focus from highlighting around the modal
+ e.target.blur();
+ if (e.key === 'ArrowLeft') {
+ this.props.onCycleThroughAttachments(-1);
+ }
+ if (e.key === 'ArrowRight') {
+ this.props.onCycleThroughAttachments(1);
+ }
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+Carousel.propTypes = propTypes;
+
+export default Carousel;
diff --git a/src/components/AttachmentCarousel/CarouselActions/index.native.js b/src/components/AttachmentCarousel/CarouselActions/index.native.js
new file mode 100644
index 000000000000..ebc7b7768077
--- /dev/null
+++ b/src/components/AttachmentCarousel/CarouselActions/index.native.js
@@ -0,0 +1,79 @@
+import React, {Component} from 'react';
+import {PanResponder, Dimensions, Animated} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../../../styles/styles';
+
+const propTypes = {
+ /** Attachment that's rendered */
+ children: PropTypes.element.isRequired,
+
+ /** Callback to fire when swiping left or right */
+ onCycleThroughAttachments: PropTypes.func.isRequired,
+
+ /** Callback to handle a press event */
+ onPress: PropTypes.func.isRequired,
+
+ /** Boolean to prevent a left swipe action */
+ canSwipeLeft: PropTypes.bool.isRequired,
+
+ /** Boolean to prevent a right swipe action */
+ canSwipeRight: PropTypes.bool.isRequired,
+};
+
+class Carousel extends Component {
+ constructor(props) {
+ super(props);
+ this.pan = new Animated.Value(0);
+
+ this.panResponder = PanResponder.create({
+ onStartShouldSetPanResponder: () => true,
+
+ onPanResponderMove: (event, gestureState) => Animated.event([null, {
+ dx: this.pan,
+ }], {useNativeDriver: false})(event, gestureState),
+
+ onPanResponderRelease: (event, gestureState) => {
+ if (gestureState.dx === 0 && gestureState.dy === 0) {
+ return this.props.onPress();
+ }
+
+ const deltaSlide = gestureState.dx > 0 ? 1 : -1;
+ if (Math.abs(gestureState.vx) < 1 || (deltaSlide === 1 && !this.props.canSwipeLeft) || (deltaSlide === -1 && !this.props.canSwipeRight)) {
+ return Animated.spring(this.pan, {useNativeDriver: false, toValue: 0}).start();
+ }
+
+ const width = Dimensions.get('window').width;
+ const slideLength = deltaSlide * (width * 1.1);
+ Animated.timing(this.pan, {useNativeDriver: false, duration: 100, toValue: slideLength}).start(({finished}) => {
+ if (!finished) {
+ return;
+ }
+
+ this.props.onCycleThroughAttachments(-deltaSlide);
+ this.pan.setValue(-slideLength);
+ Animated.timing(this.pan, {useNativeDriver: false, duration: 100, toValue: 0}).start();
+ });
+ },
+ });
+ }
+
+ render() {
+ return (
+
+ {this.props.children}
+
+ );
+ }
+}
+
+Carousel.propTypes = propTypes;
+
+export default Carousel;
diff --git a/src/components/AttachmentCarousel/index.js b/src/components/AttachmentCarousel/index.js
new file mode 100644
index 000000000000..144d0aaa0874
--- /dev/null
+++ b/src/components/AttachmentCarousel/index.js
@@ -0,0 +1,215 @@
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+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 * as ReportActionsUtils from '../../libs/ReportActionsUtils';
+import AttachmentView from '../AttachmentView';
+import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL';
+import * as DeviceCapabilities from '../../libs/DeviceCapabilities';
+import CONST from '../../CONST';
+import ONYXKEYS from '../../ONYXKEYS';
+import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes';
+import tryResolveUrlFromApiRoot from '../../libs/tryResolveUrlFromApiRoot';
+
+const propTypes = {
+ /** source is used to determine the starting index in the array of attachments */
+ source: PropTypes.string,
+
+ /** Callback to update the parent modal's state with a source and name from the attachments array */
+ onNavigate: PropTypes.func,
+
+ /** Object of report actions for this report */
+ reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),
+};
+
+const defaultProps = {
+ source: '',
+ reportActions: {},
+ onNavigate: () => {},
+};
+
+class AttachmentCarousel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
+ this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
+
+ this.state = {
+ source: this.props.source,
+ shouldShowArrow: this.canUseTouchScreen,
+ isForwardDisabled: true,
+ isBackDisabled: true,
+ };
+ }
+
+ componentDidMount() {
+ this.makeStateWithReports();
+ }
+
+ componentDidUpdate(prevProps) {
+ const previousReportActionsCount = _.size(prevProps.reportActions);
+ const currentReportActionsCount = _.size(this.props.reportActions);
+ if (previousReportActionsCount === currentReportActionsCount) {
+ return;
+ }
+ this.makeStateWithReports();
+ }
+
+ /**
+ * Helps to navigate between next/previous attachments
+ * @param {Object} attachmentItem
+ * @returns {Object}
+ */
+ getAttachment(attachmentItem) {
+ const source = _.get(attachmentItem, 'source', '');
+ const file = _.get(attachmentItem, 'file', {name: ''});
+ this.props.onNavigate({source: addEncryptedAuthTokenToURL(source), file});
+
+ return {
+ source,
+ file,
+ };
+ }
+
+ /**
+ * Toggles the visibility of the arrows
+ * @param {Boolean} shouldShowArrow
+ */
+ toggleArrowsVisibility(shouldShowArrow) {
+ this.setState({shouldShowArrow});
+ }
+
+ /**
+ * This is called when there are new reports to set the state
+ */
+ makeStateWithReports() {
+ let page;
+ const actions = ReportActionsUtils.getSortedReportActions(_.values(this.props.reportActions), true);
+
+ /**
+ * Looping to filter out attachments and retrieve the src URL and name of attachments.
+ */
+ const attachments = [];
+ _.forEach(actions, ({originalMessage, message}) => {
+ // Check for attachment which hasn't been deleted
+ if (!originalMessage || !originalMessage.html || _.some(message, m => m.isEdited)) {
+ return;
+ }
+ const matches = [...originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA)];
+
+ // matchAll captured both source url and name of the attachment
+ if (matches.length === 2) {
+ const [originalSource, name] = _.map(matches, m => m[2]);
+
+ // Update the image URL so the images can be accessed depending on the config environment.
+ // Eg: while using Ngrok the image path is from an Ngrok URL and not an Expensify URL.
+ const source = tryResolveUrlFromApiRoot(originalSource);
+ if (source === this.state.source) {
+ page = attachments.length;
+ }
+
+ attachments.push({source, file: {name}});
+ }
+ });
+
+ const {file} = this.getAttachment(attachments[page]);
+ this.setState({
+ page,
+ attachments,
+ file,
+ isForwardDisabled: page === 0,
+ isBackDisabled: page === attachments.length - 1,
+ });
+ }
+
+ /**
+ * Increments or decrements the index to get another selected item
+ * @param {Number} deltaSlide
+ */
+ cycleThroughAttachments(deltaSlide) {
+ if ((deltaSlide > 0 && this.state.isForwardDisabled) || (deltaSlide < 0 && this.state.isBackDisabled)) {
+ return;
+ }
+
+ this.setState(({attachments, page}) => {
+ const nextIndex = page - deltaSlide;
+ const {source, file} = this.getAttachment(attachments[nextIndex]);
+ return {
+ page: nextIndex,
+ source,
+ file,
+ isBackDisabled: nextIndex === attachments.length - 1,
+ isForwardDisabled: nextIndex === 0,
+ };
+ });
+ }
+
+ render() {
+ const isPageSet = Number.isInteger(this.state.page);
+ const authSource = addEncryptedAuthTokenToURL(this.state.source);
+ return (
+ this.toggleArrowsVisibility(true)}
+ onMouseLeave={() => this.toggleArrowsVisibility(false)}
+ >
+ {(isPageSet && this.state.shouldShowArrow) && (
+ <>
+ {!this.state.isBackDisabled && (
+
+ );
+ }
+}
+
+AttachmentCarousel.propTypes = propTypes;
+AttachmentCarousel.defaultProps = defaultProps;
+
+export default withOnyx({
+ reportActions: {
+ key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
+ canEvict: false,
+ },
+})(AttachmentCarousel);
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index baa459e01640..034821a8af56 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -6,13 +6,14 @@ import lodashGet from 'lodash/get';
import lodashExtend from 'lodash/extend';
import _ from 'underscore';
import CONST from '../CONST';
+import Navigation from '../libs/Navigation/Navigation';
import Modal from './Modal';
import AttachmentView from './AttachmentView';
+import AttachmentCarousel from './AttachmentCarousel';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
import * as FileUtils from '../libs/fileDownload/FileUtils';
import themeColors from '../styles/themes/default';
-import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
import compose from '../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
import Button from './Button';
@@ -61,7 +62,7 @@ const propTypes = {
const defaultProps = {
source: '',
onConfirm: null,
- originalFileName: null,
+ originalFileName: '',
isAuthTokenRequired: false,
allowDownload: false,
headerTitle: null,
@@ -74,11 +75,11 @@ class AttachmentModal extends PureComponent {
this.state = {
isModalOpen: false,
+ reportID: null,
shouldLoadAttachment: false,
isAttachmentInvalid: false,
attachmentInvalidReasonTitle: null,
attachmentInvalidReason: null,
- file: null,
source: props.source,
modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
isConfirmButtonDisabled: false,
@@ -87,10 +88,20 @@ class AttachmentModal extends PureComponent {
this.submitAndClose = this.submitAndClose.bind(this);
this.closeConfirmModal = this.closeConfirmModal.bind(this);
+ this.onNavigate = this.onNavigate.bind(this);
this.validateAndDisplayFileToUpload = this.validateAndDisplayFileToUpload.bind(this);
this.updateConfirmButtonVisibility = this.updateConfirmButtonVisibility.bind(this);
}
+ /**
+ * Helps to navigate between next/previous attachments
+ * by setting sourceURL and file in state
+ * @param {Object} attachmentData
+ */
+ onNavigate(attachmentData) {
+ this.setState(attachmentData);
+ }
+
/**
* If our attachment is a PDF, return the unswipeable Modal type.
* @param {String} sourceURL
@@ -116,7 +127,8 @@ class AttachmentModal extends PureComponent {
* @param {String} sourceURL
*/
downloadAttachment(sourceURL) {
- fileDownload(this.props.isAuthTokenRequired ? addEncryptedAuthTokenToURL(sourceURL) : sourceURL, this.props.originalFileName);
+ const originalFileName = lodashGet(this.state, 'file.name') || this.props.originalFileName;
+ fileDownload(sourceURL, originalFileName);
// At ios, if the keyboard is open while opening the attachment, then after downloading
// the attachment keyboard will show up. So, to fix it we need to dismiss the keyboard.
@@ -230,9 +242,7 @@ class AttachmentModal extends PureComponent {
}
render() {
- // If source is a URL, add auth token to get access
const source = this.state.source;
-
return (
<>
this.downloadAttachment(source)}
onCloseButtonPress={() => this.setState({isModalOpen: false})}
/>
-
- {this.state.source && this.state.shouldLoadAttachment && (
+ {this.state.reportID ? (
+
+ ) : this.state.source && this.state.shouldLoadAttachment && (
)}
-
{/* If we have an onConfirm method show a confirmation button */}
{this.props.onConfirm && (
@@ -302,7 +317,12 @@ class AttachmentModal extends PureComponent {
{this.props.children({
displayFileInModal: this.validateAndDisplayFileToUpload,
show: () => {
- this.setState({isModalOpen: true});
+ const route = Navigation.getActiveRoute();
+ let reportID = null;
+ if (route.includes('/r/')) {
+ reportID = route.replace('/r/', '');
+ }
+ this.setState({isModalOpen: true, reportID});
},
})}
>
diff --git a/src/components/AttachmentView.js b/src/components/AttachmentView.js
index 3381a35f0d39..5e5221482350 100755
--- a/src/components/AttachmentView.js
+++ b/src/components/AttachmentView.js
@@ -34,6 +34,9 @@ const propTypes = {
/** Flag to show the loading indicator */
shouldShowLoadingSpinnerIcon: PropTypes.bool,
+ /** Function for handle on press */
+ onPress: PropTypes.func,
+
/** Notify parent that the UI should be modified to accommodate keyboard */
onToggleKeyboard: PropTypes.func,
@@ -47,6 +50,7 @@ const defaultProps = {
},
shouldShowDownloadIcon: false,
shouldShowLoadingSpinnerIcon: false,
+ onPress: () => {},
onToggleKeyboard: () => {},
};
@@ -67,6 +71,7 @@ const AttachmentView = (props) => {
: props.source;
return (
{
// both PDFs and images will appear as images when pasted into the the text field
if (Str.isImage(props.source) || (props.file && Str.isImage(props.file.name))) {
return (
-
+
);
}
diff --git a/src/components/Image/index.js b/src/components/Image/index.js
index 1c67dcf35129..0b42f3ee25d7 100644
--- a/src/components/Image/index.js
+++ b/src/components/Image/index.js
@@ -2,6 +2,7 @@ import React from 'react';
import {Image as RNImage} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
+import _ from 'underscore';
import ONYXKEYS from '../../ONYXKEYS';
import {defaultProps, imagePropTypes} from './imagePropTypes';
import RESIZE_MODES from './resizeModes';
@@ -10,20 +11,24 @@ class Image extends React.Component {
constructor(props) {
super(props);
+ this.debouncedConfigureImageSource = _.debounce(this.configureImageSource, 220);
+
this.state = {
imageSource: undefined,
};
}
componentDidMount() {
- this.configureImageSource();
+ this.debouncedConfigureImageSource();
}
componentDidUpdate(prevProps) {
- if (prevProps.source === this.props.source) {
+ if (prevProps.source.uri === this.props.source.uri) {
return;
}
- this.configureImageSource();
+
+ this.debouncedConfigureImageSource.cancel();
+ this.debouncedConfigureImageSource();
}
/**
@@ -32,6 +37,7 @@ class Image extends React.Component {
* and as a result the `onLoad` event needs to be maunually invoked to return these dimensions
*/
configureImageSource() {
+ this.props.onLoadStart();
const source = this.props.source;
let imageSource = source;
if (this.props.isAuthTokenRequired) {
@@ -48,6 +54,7 @@ class Image extends React.Component {
if (this.props.onLoad == null) {
return;
}
+
RNImage.getSize(imageSource.uri, (width, height) => {
this.props.onLoad({nativeEvent: {width, height}});
});
diff --git a/src/components/ImageView/index.js b/src/components/ImageView/index.js
index 67af54a03141..8c48da7833cd 100644
--- a/src/components/ImageView/index.js
+++ b/src/components/ImageView/index.js
@@ -31,7 +31,6 @@ class ImageView extends PureComponent {
this.onContainerPress = this.onContainerPress.bind(this);
this.imageLoad = this.imageLoad.bind(this);
this.imageLoadingStart = this.imageLoadingStart.bind(this);
- this.imageLoadingEnd = this.imageLoadingEnd.bind(this);
this.trackMovement = this.trackMovement.bind(this);
this.trackPointerPosition = this.trackPointerPosition.bind(this);
@@ -56,10 +55,19 @@ class ImageView extends PureComponent {
if (this.canUseTouchScreen) {
return;
}
+
document.addEventListener('mousemove', this.trackMovement);
document.addEventListener('mouseup', this.trackPointerPosition);
}
+ componentDidUpdate(prevProps) {
+ if (prevProps.url === this.props.url || this.state.isLoading) {
+ return;
+ }
+
+ this.imageLoadingStart();
+ }
+
componentWillUnmount() {
if (this.canUseTouchScreen) {
return;
@@ -213,14 +221,14 @@ class ImageView extends PureComponent {
imageLoad({nativeEvent}) {
this.setImageRegion(nativeEvent.width, nativeEvent.height);
+ this.setState({isLoading: false});
}
imageLoadingStart() {
- this.setState({isLoading: true});
- }
-
- imageLoadingEnd() {
- this.setState({isLoading: false});
+ if (this.state.isLoading) {
+ return;
+ }
+ this.setState({isLoading: true, zoomScale: 0, isZoomed: false});
}
render() {
@@ -233,16 +241,17 @@ class ImageView extends PureComponent {
1 ? Image.resizeMode.center : Image.resizeMode.contain}
onLoadStart={this.imageLoadingStart}
- onLoadEnd={this.imageLoadingEnd}
onLoad={this.imageLoad}
/>
{this.state.isLoading && (
@@ -276,14 +285,12 @@ class ImageView extends PureComponent {
>
diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js
index a7966a4b0b04..70ce7b907e98 100644
--- a/src/components/ImageView/index.native.js
+++ b/src/components/ImageView/index.native.js
@@ -21,11 +21,15 @@ const propTypes = {
/** URL to full-sized image */
url: PropTypes.string.isRequired,
+ /** Function for handle on press */
+ onPress: PropTypes.func,
+
...windowDimensionsPropTypes,
};
const defaultProps = {
isAuthTokenRequired: false,
+ onPress: () => {},
};
class ImageView extends PureComponent {
@@ -37,7 +41,6 @@ class ImageView extends PureComponent {
imageWidth: 0,
imageHeight: 0,
interactionPromise: undefined,
- containerHeight: undefined,
};
// Use the default double click interval from the ImageZoom library
@@ -53,6 +56,19 @@ class ImageView extends PureComponent {
});
this.configureImageZoom = this.configureImageZoom.bind(this);
+ this.imageLoadingStart = this.imageLoadingStart.bind(this);
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.url === prevProps.url) {
+ return;
+ }
+
+ this.imageLoadingStart();
+
+ if (this.interactionPromise) {
+ this.state.interactionPromise.cancel();
+ }
}
componentWillUnmount() {
@@ -91,7 +107,7 @@ class ImageView extends PureComponent {
let imageWidth = nativeEvent.width;
let imageHeight = nativeEvent.height;
const containerWidth = Math.round(this.props.windowWidth);
- const containerHeight = Math.round(this.state.containerHeight);
+ const containerHeight = Math.round(this.state.containerHeight ? this.state.containerHeight : this.props.windowHeight);
const aspectRatio = Math.min(containerHeight / imageHeight, containerWidth / imageWidth);
@@ -103,12 +119,39 @@ class ImageView extends PureComponent {
// Resize the image to max dimensions possible on the Native platforms to prevent crashes on Android. To keep the same behavior, apply to IOS as well.
const maxDimensionsScale = 11;
- imageHeight = Math.min(imageHeight, (this.props.windowHeight * maxDimensionsScale));
- imageWidth = Math.min(imageWidth, (this.props.windowWidth * maxDimensionsScale));
+ imageWidth = Math.min(imageWidth, (containerWidth * maxDimensionsScale));
+ imageHeight = Math.min(imageHeight, (containerHeight * maxDimensionsScale));
this.setState({imageHeight, imageWidth, isLoading: false});
});
}
+ /**
+ * When the url changes and the image must load again,
+ * this resets the zoom to ensure the next image loads with the correct dimensions.
+ */
+ resetImageZoom() {
+ if (this.imageZoomScale !== 1) {
+ this.imageZoomScale = 1;
+ }
+
+ if (this.zoom) {
+ this.zoom.centerOn({
+ x: 0,
+ y: 0,
+ scale: 1,
+ duration: 0,
+ });
+ }
+ }
+
+ imageLoadingStart() {
+ if (this.state.isLoading) {
+ return;
+ }
+ this.resetImageZoom();
+ this.setState({imageHeight: 0, imageWidth: 0, isLoading: true});
+ }
+
render() {
// Default windowHeight accounts for the modal header height
const windowHeight = this.props.windowHeight - variables.contentHeaderHeight;
@@ -135,6 +178,7 @@ class ImageView extends PureComponent {
{this.state.containerHeight && (
this.zoom = el}
+ onClick={() => this.props.onPress()}
cropWidth={this.props.windowWidth}
cropHeight={windowHeight}
imageWidth={this.state.imageWidth}
@@ -180,6 +224,7 @@ class ImageView extends PureComponent {
source={{uri: this.props.url}}
isAuthTokenRequired={this.props.isAuthTokenRequired}
resizeMode={Image.resizeMode.contain}
+ onLoadStart={this.imageLoadingStart}
onLoad={this.configureImageZoom}
/>
{/**
diff --git a/src/components/PDFView/index.native.js b/src/components/PDFView/index.native.js
index ade5d01353d1..a0f6669ebcef 100644
--- a/src/components/PDFView/index.native.js
+++ b/src/components/PDFView/index.native.js
@@ -161,6 +161,7 @@ class PDFView extends Component {
onError={this.handleFailureToLoadPDF}
password={this.state.password}
onLoadComplete={this.finishPDFLoad}
+ onPageSingleTap={this.props.onPress}
/>
)}
diff --git a/src/components/PDFView/pdfViewPropTypes.js b/src/components/PDFView/pdfViewPropTypes.js
index 281c135fe438..3a17d3802b4f 100644
--- a/src/components/PDFView/pdfViewPropTypes.js
+++ b/src/components/PDFView/pdfViewPropTypes.js
@@ -12,12 +12,16 @@ const propTypes = {
/** Notify parent that the keyboard has opened or closed */
onToggleKeyboard: PropTypes.func,
+ /** Handles press events like toggling attachment arrows natively */
+ onPress: PropTypes.func,
+
...windowDimensionsPropTypes,
};
const defaultProps = {
sourceURL: '',
style: {},
+ onPress: () => {},
onToggleKeyboard: () => {},
};
diff --git a/src/styles/styles.js b/src/styles/styles.js
index 88a8ea989d89..fe71c5274eff 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -2138,6 +2138,31 @@ const styles = {
alignSelf: 'flex-start',
},
+ attachmentModalArrowsContainer: {
+ display: 'flex',
+ justifyContent: 'center',
+ cursor: 'unset',
+ height: '100%',
+ width: '100%',
+ },
+
+ leftAttachmentArrow: {
+ zIndex: 23,
+ position: 'absolute',
+ left: 32,
+ },
+
+ rightAttachmentArrow: {
+ zIndex: 23,
+ position: 'absolute',
+ right: 32,
+ },
+
+ arrowIcon: {
+ height: 52,
+ width: 52,
+ },
+
detailsPageSectionVersion: {
alignSelf: 'center',
color: themeColors.textSupporting,