Skip to content

Commit

Permalink
adding carousel to attachment modal
Browse files Browse the repository at this point in the history
  • Loading branch information
JediWattson committed Jul 20, 2022
1 parent 6369e8e commit c216164
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 6 deletions.
3 changes: 3 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -689,6 +689,9 @@ const CONST = {
EMOJIS: /(?:\uD83D(?:\uDC41\u200D\uD83D\uDDE8|\uDC68\u200D\uD83D[\uDC68\uDC69]\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?)|\uDC69\u200D\uD83D\uDC69\u200D\uD83D(?:\uDC66(?:\u200D\uD83D\uDC66)?|\uDC67(?:\u200D\uD83D[\uDC66\uDC67])?))|[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g,
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,
},

PRONOUNS: {
Expand Down
114 changes: 114 additions & 0 deletions src/components/AttachmentCarousel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';
import {Pressable, View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import _ from 'lodash';
import styles from '../styles/styles';
import * as Expensicons from './Icon/Expensicons';
import variables from '../styles/variables';
import Icon from './Icon';
import reportActionPropTypes from '../pages/home/report/reportActionPropTypes';
import CONFIG from '../CONFIG';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';

const propTypes = {
/** sourceUrl is used to determine the starting index in the array of attachments */
sourceURL: PropTypes.string,

/** Object of report actions for this report */
reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)),

/** Callback to update the parent modal's state with a sourceUrl and name from the attachments array */
onArrowPress: PropTypes.func,
};

const defaultProps = {
sourceURL: '',
reportActions: {},
onArrowPress: () => {},
};

class AttachmentCarousel extends React.Component {
constructor(props) {
super(props);

let page;
const actionsArr = _.values(props.reportActions);
const attachments = _.reduce(actionsArr, (attachmentsAccumulator, reportAction) => {
if (reportAction.originalMessage && reportAction.originalMessage.html) {
const matchesIt = reportAction.originalMessage.html.matchAll(CONST.REGEX.ATTACHMENT_DATA);
const matches = [...matchesIt];
if (matches.length === 2) {
const [src, name] = matches;
if (src[2].includes(props.sourceURL)) {
page = attachmentsAccumulator.length;
}
const url = src[2].replace(
CONFIG.EXPENSIFY.EXPENSIFY_URL,
CONFIG.EXPENSIFY.URL_API_ROOT,
);
attachmentsAccumulator.push({sourceURL: url, file: {name: name[2]}});
}
}
return attachmentsAccumulator;
}, []);

this.state = {
page,
attachments,
};

this.cycleThroughAttachments = this.cycleThroughAttachments.bind(this);
}

/**
* increments or decrements the index to get another selected item
* @param {Boolean} shouldDecrement
*/
cycleThroughAttachments(shouldDecrement) {
const attachments = this.state.attachments;
const page = this.state.page;
if (shouldDecrement ? page - 1 < 0 : page + 1 === attachments.length) {
return;
}

const nextIndex = shouldDecrement ? page - 1 : page + 1;
this.props.onArrowPress(attachments[nextIndex]);
this.setState({page: nextIndex});
}

renderPressableView(isBackArrow) {
return (
<View style={[styles.cursorPointer, styles.attachmentModalArrowsIcon]}>
<Pressable onPress={() => this.cycleThroughAttachments(isBackArrow)}>
<Icon
height={variables.iconSizeNormal}
width={variables.iconSizeNormal}
fill="black"
src={isBackArrow ? Expensicons.BackArrow : Expensicons.ArrowRight}
/>
</Pressable>
</View>
);
}

render() {
return (
<View style={[styles.attachmentModalArrowsContainer]}>
{this.renderPressableView(true)}
{this.renderPressableView(false)}
</View>
);
}
}

AttachmentCarousel.propTypes = propTypes;
AttachmentCarousel.defaultProps = defaultProps;

export default withOnyx({
reportActions: {
key: ({reportId}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportId}`,
canEvict: true,
},
})(AttachmentCarousel);
57 changes: 51 additions & 6 deletions src/components/AttachmentModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import _ from 'lodash';
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 themeColors from '../styles/themes/default';
import addEncryptedAuthTokenToURL from '../libs/addEncryptedAuthTokenToURL';
Expand Down Expand Up @@ -58,7 +60,7 @@ const propTypes = {
const defaultProps = {
sourceURL: null,
onConfirm: null,
originalFileName: null,
originalFileName: '',
isAuthTokenRequired: false,
allowDownload: false,
headerTitle: null,
Expand All @@ -70,16 +72,47 @@ class AttachmentModal extends PureComponent {
super(props);

this.state = {
page: -1,
attachments: [],
isModalOpen: false,
isConfirmModalOpen: false,
file: null,
reportId: null,
file: {name: lodashGet(props, 'originalFileName', '')},
sourceURL: props.sourceURL,
modalType: CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE,
};

this.submitAndClose = this.submitAndClose.bind(this);
this.closeConfirmModal = this.closeConfirmModal.bind(this);
this.isValidSize = this.isValidSize.bind(this);
this.onArrowPress = this.onArrowPress.bind(this);
}

// this prevents a bug in iOS that would show the last image before closing then opening on a new image
static getDerivedStateFromProps(props, state) {
if (state.isModalOpen && state.isModalOpen !== state.prevIsModalOpen) {
return {
prevIsModalOpen: true,
file: {name: lodashGet(props, 'originalFileName', '')},
sourceURL: lodashGet(props, 'sourceURL', ''),
};
}

if (!state.isModalOpen && state.isModalOpen !== state.prevIsModalOpen) {
return {prevIsModalOpen: false};
}

return null;
}

/**
* callback in used in AttachmentCarousel to delegate when a user presses an arrow
* @param {Object} attachmentItem
*/
onArrowPress(attachmentItem) {
const sourceURL = lodashGet(attachmentItem, 'sourceURL', '');
const file = lodashGet(attachmentItem, 'file', {name: ''});
this.setState({sourceURL, file});
}

/**
Expand All @@ -101,7 +134,7 @@ class AttachmentModal extends PureComponent {
* @returns {Object}
*/
splitExtensionFromFileName() {
const fullFileName = this.props.originalFileName ? this.props.originalFileName.trim() : lodashGet(this.state, 'file.name', '').trim();
const fullFileName = lodashGet(this.state, 'file.name', '').trim();
const splittedFileName = fullFileName.split('.');
const fileExtension = splittedFileName.pop();
const fileName = splittedFileName.join('.');
Expand Down Expand Up @@ -180,9 +213,16 @@ class AttachmentModal extends PureComponent {
) : ''}
/>
<View style={attachmentViewStyles}>
{this.state.sourceURL && (
<>
<AttachmentView sourceURL={sourceURL} file={this.state.file} />
)}
{this.state.reportId && (
<AttachmentCarousel
reportId={this.state.reportId}
onArrowPress={this.onArrowPress}
sourceURL={this.state.sourceURL}
/>
)}
</>
</View>

{/* If we have an onConfirm method show a confirmation button */}
Expand Down Expand Up @@ -226,7 +266,12 @@ class AttachmentModal extends PureComponent {
}
},
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});
},
})}
</>
Expand Down
9 changes: 9 additions & 0 deletions src/components/ImageView/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ class ImageView extends PureComponent {
document.addEventListener('mousemove', this.trackMovement.bind(this));
}

componentDidUpdate() {
if (this.canUseTouchScreen) {
return;
}
Image.getSize(this.props.url, (width, height) => {
this.setImageRegion(width, height);
});
}

componentWillUnmount() {
if (this.canUseTouchScreen) {
return;
Expand Down
17 changes: 17 additions & 0 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -2012,6 +2012,23 @@ const styles = {
alignSelf: 'flex-start',
},

attachmentModalArrowsContainer: {
width: '90%',
position: 'absolute',
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
},

attachmentModalArrowsIcon: {
height: variables.iconSizeNormal + 10,
width: variables.iconSizeNormal + 10,
borderRadius: variables.iconSizeNormal + 10,
paddingTop: 5,
paddingLeft: 5,
backgroundColor: 'lightgrey',
},

detailsPageSectionVersion: {
alignSelf: 'center',
color: themeColors.textSupporting,
Expand Down

0 comments on commit c216164

Please sign in to comment.