-
Notifications
You must be signed in to change notification settings - Fork 3.5k
adding carousel to attachment modal #9279
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
Merged
Merged
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 519925d
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 962aaf2
removed undefined function
JediWattson 0cb9e60
use dimension height to get image height
JediWattson 33bd189
added cancel when image changes
JediWattson a09b349
fixed hook
JediWattson f9948a6
added loading indicator when event is handled, removed logic preventi…
JediWattson 674af4f
added back layout for issue with size
JediWattson 3d2dd78
Merge branch 'main' into arrow-feature-signed
JediWattson 80d6746
fixing lint issues
JediWattson 180c52b
changed var name
JediWattson 1883524
reverting action load
JediWattson 19aa567
fixed logic to allow cursor index to change
JediWattson b58bfde
linting
JediWattson 011f4cc
removed unused function, use props to constant url associated with image
JediWattson 99d7126
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson a853953
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 5ae7925
fixed glitchy issue with imgs, check if deleted
JediWattson b1f2fdb
fixing lint
JediWattson 7bfbb1f
added on press event for arrow
JediWattson ccb8cbd
add default
JediWattson 390e6ed
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 34dc3ea
Merge branch 'main' into arrow-feature-signed
JediWattson e49b52c
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 1ea47cc
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 7e2861a
set zoom to false when changing images
JediWattson 67f955f
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 37b3fd9
fixed issue with zoom, set zoom to 1 on mobile
JediWattson b6ce704
lint fixes
JediWattson cec9ba8
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 43ad6e7
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson c3715f7
Merge branch 'main' into arrow-feature-signed
JediWattson 87acc95
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 4c32e2d
added logic for paging for ngrok
JediWattson 2d6eb88
fixed worker import to load pdf
JediWattson 7a1dcf8
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson c151e73
use tryResolveUrlFromApiRoot to get correct urls
JediWattson b0fc2dc
Revert "fixed worker import to load pdf"
JediWattson f6eb54a
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 6363c52
removed extraspaces
JediWattson 349ccdb
ran lint fix
JediWattson 1934508
adding loading when getting new attachments
JediWattson f03ac5a
added check for when no attachments get loaded
JediWattson 3daf910
Merge branch 'main' into arrow-feature-signed
JediWattson 6a720dd
Revert "added check for when no attachments get loaded"
JediWattson 8d419e4
Revert "adding loading when getting new attachments"
JediWattson 60b3962
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson ac3d4dc
removed code for updating actions
JediWattson 1982c49
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 6bfc8bb
fixed up logic and some comments
JediWattson 8304865
fixed isLoading to show indicator
JediWattson 778374a
linting
JediWattson d476dc7
switched values in arrows to make intuitive
JediWattson 740adee
fixed the rest of the arrows
JediWattson 7ecfdb2
Merge branch 'Expensify:main' into arrow-feature-signed
JediWattson 3fc8169
Merge branch 'main' into arrow-feature-signed
JediWattson 17e1fd1
enhancing loading when changing urls
JediWattson 9e178e2
removed redundant call
JediWattson 483a8b7
added back some functions, made sure redundant do not happen
JediWattson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
src/components/AttachmentCarousel/CarouselActions/index.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| 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; | ||
79 changes: 79 additions & 0 deletions
79
src/components/AttachmentCarousel/CarouselActions/index.native.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.