Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding carousel to attachment modal #9279

Merged
merged 59 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
edd1455
Squashed commit of the following:
JediWattson Jan 26, 2023
519925d
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Jan 26, 2023
962aaf2
removed undefined function
JediWattson Jan 26, 2023
0cb9e60
use dimension height to get image height
JediWattson Jan 27, 2023
33bd189
added cancel when image changes
JediWattson Jan 27, 2023
a09b349
fixed hook
JediWattson Jan 27, 2023
f9948a6
added loading indicator when event is handled, removed logic preventi…
JediWattson Jan 30, 2023
674af4f
added back layout for issue with size
JediWattson Jan 31, 2023
3d2dd78
Merge branch 'main' into arrow-feature-signed
JediWattson Feb 2, 2023
80d6746
fixing lint issues
JediWattson Feb 2, 2023
180c52b
changed var name
JediWattson Feb 2, 2023
1883524
reverting action load
JediWattson Feb 2, 2023
19aa567
fixed logic to allow cursor index to change
JediWattson Feb 3, 2023
b58bfde
linting
JediWattson Feb 3, 2023
011f4cc
removed unused function, use props to constant url associated with image
JediWattson Feb 4, 2023
99d7126
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 4, 2023
a853953
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 7, 2023
5ae7925
fixed glitchy issue with imgs, check if deleted
JediWattson Feb 8, 2023
b1f2fdb
fixing lint
JediWattson Feb 8, 2023
7bfbb1f
added on press event for arrow
JediWattson Feb 9, 2023
ccb8cbd
add default
JediWattson Feb 9, 2023
390e6ed
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 9, 2023
34dc3ea
Merge branch 'main' into arrow-feature-signed
JediWattson Feb 11, 2023
e49b52c
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 13, 2023
1ea47cc
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 13, 2023
7e2861a
set zoom to false when changing images
JediWattson Feb 13, 2023
67f955f
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 13, 2023
37b3fd9
fixed issue with zoom, set zoom to 1 on mobile
JediWattson Feb 15, 2023
b6ce704
lint fixes
JediWattson Feb 15, 2023
cec9ba8
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 15, 2023
43ad6e7
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 15, 2023
c3715f7
Merge branch 'main' into arrow-feature-signed
JediWattson Feb 17, 2023
87acc95
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 21, 2023
4c32e2d
added logic for paging for ngrok
JediWattson Feb 21, 2023
2d6eb88
fixed worker import to load pdf
JediWattson Feb 21, 2023
7a1dcf8
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 22, 2023
c151e73
use tryResolveUrlFromApiRoot to get correct urls
JediWattson Feb 22, 2023
b0fc2dc
Revert "fixed worker import to load pdf"
JediWattson Feb 22, 2023
f6eb54a
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 23, 2023
6363c52
removed extraspaces
JediWattson Feb 24, 2023
349ccdb
ran lint fix
JediWattson Feb 24, 2023
1934508
adding loading when getting new attachments
JediWattson Feb 24, 2023
f03ac5a
added check for when no attachments get loaded
JediWattson Feb 24, 2023
3daf910
Merge branch 'main' into arrow-feature-signed
JediWattson Feb 24, 2023
6a720dd
Revert "added check for when no attachments get loaded"
JediWattson Feb 27, 2023
8d419e4
Revert "adding loading when getting new attachments"
JediWattson Feb 27, 2023
60b3962
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 27, 2023
ac3d4dc
removed code for updating actions
JediWattson Feb 28, 2023
1982c49
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Feb 28, 2023
6bfc8bb
fixed up logic and some comments
JediWattson Mar 1, 2023
8304865
fixed isLoading to show indicator
JediWattson Mar 1, 2023
778374a
linting
JediWattson Mar 1, 2023
d476dc7
switched values in arrows to make intuitive
JediWattson Mar 1, 2023
740adee
fixed the rest of the arrows
JediWattson Mar 4, 2023
7ecfdb2
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson Mar 4, 2023
3fc8169
Merge branch 'main' into arrow-feature-signed
JediWattson Mar 6, 2023
17e1fd1
enhancing loading when changing urls
JediWattson Mar 6, 2023
9e178e2
removed redundant call
JediWattson Mar 6, 2023
483a8b7
added back some functions, made sure redundant do not happen
JediWattson Mar 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,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}$/,
Expand Down
64 changes: 64 additions & 0 deletions src/components/AttachmentCarousel/CarouselActions/index.js
Original file line number Diff line number Diff line change
@@ -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();
luacmartins marked this conversation as resolved.
Show resolved Hide resolved
if (e.key === 'ArrowLeft') {
this.props.onCycleThroughAttachments(-1);
}
if (e.key === 'ArrowRight') {
this.props.onCycleThroughAttachments(1);
}
}

render() {
return (
<Pressable style={this.props.styles} onPress={this.props.onPress}>
{this.props.children}
</Pressable>
);
}
}

Carousel.propTypes = propTypes;

export default Carousel;
Original file line number Diff line number Diff line change
@@ -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,
chiragsalian marked this conversation as resolved.
Show resolved Hide resolved

/** 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 (
<Animated.View
style={[
styles.w100,
styles.h100,
{transform: [{translateX: this.pan}]},
]}
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.panResponder.panHandlers}
>
{this.props.children}
</Animated.View>
);
}
}

Carousel.propTypes = propTypes;

export default Carousel;
215 changes: 215 additions & 0 deletions src/components/AttachmentCarousel/index.js
Original file line number Diff line number Diff line change
@@ -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() {
chiragsalian marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is weird, logic

consider if there is only one attachment in loaded chats and there is more to load from history with back being disabled we can't view it.

});
}

/**
* 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 (
<View
style={[styles.attachmentModalArrowsContainer]}
onMouseEnter={() => this.toggleArrowsVisibility(true)}
onMouseLeave={() => this.toggleArrowsVisibility(false)}
>
{(isPageSet && this.state.shouldShowArrow) && (
<>
{!this.state.isBackDisabled && (
<Button
medium
style={[styles.leftAttachmentArrow]}
innerStyles={[styles.arrowIcon]}
icon={Expensicons.BackArrow}
iconFill={themeColors.text}
iconStyles={[styles.mr0]}
onPress={() => this.cycleThroughAttachments(-1)}
/>
)}
{!this.state.isForwardDisabled && (
<Button
medium
style={[styles.rightAttachmentArrow]}
innerStyles={[styles.arrowIcon]}
icon={Expensicons.ArrowRight}
iconFill={themeColors.text}
iconStyles={[styles.mr0]}
onPress={() => this.cycleThroughAttachments(1)}
/>
)}
</>
)}
<CarouselActions
styles={[styles.attachmentModalArrowsContainer]}
canSwipeLeft={!this.state.isBackDisabled}
canSwipeRight={!this.state.isForwardDisabled}
onPress={() => this.canUseTouchScreen && this.toggleArrowsVisibility(!this.state.shouldShowArrow)}
onCycleThroughAttachments={this.cycleThroughAttachments}
>
<AttachmentView
onPress={() => this.toggleArrowsVisibility(!this.state.shouldShowArrow)}
source={authSource}
file={this.state.file}
/>
</CarouselActions>
</View>
);
}
}

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

export default withOnyx({
reportActions: {
key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
canEvict: false,
},
})(AttachmentCarousel);
Loading
Loading