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

Show Loader when adding Workspace Edit #4801

Merged
merged 20 commits into from
Sep 9, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a1d2b38
fix(wkspace-edit-loader): Added loader for AvatarWithImagePicker
mananjadhav Aug 24, 2021
e90f8ff
fix(wkspace-edit-loader): Added loader for avatar + form submission
mananjadhav Aug 24, 2021
be3a8b4
Merge branch 'main' of https://github.com/mananjadhav/App into fix/wk…
mananjadhav Aug 26, 2021
0705612
fix(wkspace-edit-loader): Added upload indicator to AvatarWithImagePi…
mananjadhav Aug 30, 2021
7097670
fix(wkspace-edit-loader): Added loading indicator to Profile Image
mananjadhav Aug 30, 2021
0e77957
fix(wkspace-edit-loader): Changed flags from state to onyx props
mananjadhav Aug 30, 2021
d9afbab
fix(wskpace-edit-loader): Added function for local updates of policy …
mananjadhav Aug 30, 2021
55025ab
fix(wkspace-edit-loader): Removed return Promise and added catch to s…
mananjadhav Aug 30, 2021
fafe5ec
fix(wkspace-edit-loader): Added catch to throw error
mananjadhav Aug 30, 2021
93d4769
Merge branch 'main' of https://github.com/mananjadhav/App into fix/wk…
mananjadhav Aug 30, 2021
048d469
Merge branch 'main' of https://github.com/mananjadhav/App into fix/wk…
mananjadhav Sep 2, 2021
7713aca
fix(wkspace-editor-loader): Fixed icon size for loader
mananjadhav Sep 2, 2021
2d35e86
fix(wkspace-edit-loader): Fixed icon sizes in indicator
mananjadhav Sep 2, 2021
5dbc4eb
fix(wkspace-edit-loader): Changed recursive func call to Animate.loop
mananjadhav Sep 4, 2021
69ca002
fix(wkspace-edit-loader): Changed recursive func call to Animate.loop…
mananjadhav Sep 5, 2021
6fcdf7e
fix(wkspace-edit-loader): Moved animation to separate class with Anim…
mananjadhav Sep 7, 2021
7d51071
fix(wkspace-edit-loader): Replace animation with SpinningAnimation
mananjadhav Sep 7, 2021
9a5fad8
fix(wkspace-edit-loader): Rollback Animated.parallel and Animated.loo…
mananjadhav Sep 7, 2021
cf0de74
Merge branch 'main' of https://github.com/mananjadhav/App into fix/wk…
mananjadhav Sep 9, 2021
65f8dfa
fix(wkspace-edit-loader): Changed to Animated.loop
mananjadhav Sep 9, 2021
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
157 changes: 135 additions & 22 deletions src/components/AvatarWithImagePicker.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import _ from 'underscore';
import React from 'react';
import {Pressable, View} from 'react-native';
import {
Pressable, View, Animated, Easing, StyleSheet,
} from 'react-native';
import PropTypes from 'prop-types';
import Avatar from './Avatar';
import Icon from './Icon';
import PopoverMenu from './PopoverMenu';
import {
Upload, Trashcan, Pencil,
Upload, Trashcan, Pencil, Sync,
} from './Icon/Expensicons';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import AttachmentPicker from './AttachmentPicker';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import variables from '../styles/variables';
import {getSyncingStyles} from '../styles/getAvatarWithIndicatorStyles';

const propTypes = {
/** Avatar URL to display */
Expand Down Expand Up @@ -41,6 +44,9 @@ const propTypes = {
left: PropTypes.number,
}).isRequired,

/** Flag to see if image is being uploaded */
isUploading: PropTypes.bool,

...withLocalizePropTypes,
};

Expand All @@ -51,17 +57,96 @@ const defaultProps = {
style: [],
DefaultAvatar: () => {},
isUsingDefaultAvatar: false,
isUploading: false,
};

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

this.rotate = new Animated.Value(0);
this.scale = new Animated.Value(1);
this.startRotation = this.startRotation.bind(this);
this.startUploadIndicator = this.startUploadIndicator.bind(this);
this.stopUploadIndicator = this.stopUploadIndicator.bind(this);

this.state = {
isMenuVisible: false,
};
}

componentDidMount() {
if (this.props.isUploading) {
this.startUploadIndicator();
}
}

componentDidUpdate(prevProps) {
if (!prevProps.isUploading && this.props.isUploading) {
this.startUploadIndicator();
} else if (prevProps.isUploading && !this.props.isUploading) {
this.stopUploadIndicator();
}
}

componentWillUnmount() {
this.stopUploadIndicator();
}

/**
* We need to manually loop the animations as `useNativeDriver` does not work well with Animated.loop.
*
* @memberof AvatarWithImagePicker
*/
startRotation() {
this.rotate.setValue(0);
Animated.timing(this.rotate, {
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
toValue: 1,
duration: 2000,
easing: Easing.linear,
isInteraction: false,
useNativeDriver: true,
}).start(({finished}) => {
if (finished) {
this.startRotation();
}
});
}

/**
* Start Animation for Indicator
*
* @memberof AvatarWithImagePicker
*/
startUploadIndicator() {
this.startRotation();
Animated.spring(this.scale, {
toValue: 1.666,
tension: 1,
isInteraction: false,
useNativeDriver: true,
}).start();
}

/**
* Stop Animation for Indicator
*
* @memberof AvatarWithImagePicker
*/
stopUploadIndicator() {
Animated.spring(this.scale, {
toValue: 1,
tension: 1,
isInteraction: false,
useNativeDriver: true,
}).start(() => {
this.rotate.resetAnimation();
this.scale.resetAnimation();
this.rotate.setValue(0);
});
}


/**
* Create menu items list for avatar menu
*
Expand Down Expand Up @@ -97,6 +182,15 @@ class AvatarWithImagePicker extends React.Component {
render() {
const {DefaultAvatar} = this.props;
const additionalStyles = _.isArray(this.props.style) ? this.props.style : [this.props.style];

const indicatorStyles = [
styles.alignItemsCenter,
styles.justifyContentCenter,
this.props.size === 'large' ? styles.statusIndicatorLarge : styles.statusIndicator,
styles.statusIndicatorOnline,
getSyncingStyles(this.rotate, this.scale),
];

return (
<View style={[styles.alignItemsCenter, ...additionalStyles]}>

Expand All @@ -115,26 +209,45 @@ class AvatarWithImagePicker extends React.Component {
<AttachmentPicker>
{({openPicker}) => (
<>
<Pressable
style={[styles.smallEditIcon, styles.smallAvatarEditIcon]}
onPress={() => this.setState({isMenuVisible: true})}
>
<Icon
src={Pencil}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
fill={themeColors.iconReversed}
/>
</Pressable>
<PopoverMenu
isVisible={this.state.isMenuVisible}
onClose={() => this.setState({isMenuVisible: false})}
onItemSelected={() => this.setState({isMenuVisible: false})}
menuItems={this.createMenuItems(openPicker)}
anchorPosition={this.props.anchorPosition}
animationIn="fadeInDown"
animationOut="fadeOutUp"
/>
{
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
this.props.isUploading
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
? (
<Animated.View style={StyleSheet.flatten(indicatorStyles)}>

<Icon
src={Sync}
fill={themeColors.textReversed}
width={6}
height={6}
/>
</Animated.View>
)
: (
<>
<Pressable
disabled={this.props.isUploading}
style={[styles.smallEditIcon, styles.smallAvatarEditIcon]}
onPress={() => this.setState({isMenuVisible: true})}
>
<Icon
src={Pencil}
width={variables.iconSizeSmall}
height={variables.iconSizeSmall}
fill={themeColors.iconReversed}
/>
</Pressable>
<PopoverMenu
isVisible={this.state.isMenuVisible}
onClose={() => this.setState({isMenuVisible: false})}
onItemSelected={() => this.setState({isMenuVisible: false})}
menuItems={this.createMenuItems(openPicker)}
anchorPosition={this.props.anchorPosition}
animationIn="fadeInDown"
animationOut="fadeOutUp"
/>
</>
)
}
</>
)}
</AttachmentPicker>
Expand Down
3 changes: 2 additions & 1 deletion src/libs/actions/PersonalDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,10 +265,11 @@ function fetchLocalCurrency() {
* @param {File|Object} file
*/
function setAvatar(file) {
setPersonalDetails({avatarUploading: true});
API.User_UploadAvatar({file}).then((response) => {
// Once we get the s3url back, update the personal details for the user with the new avatar URL
if (response.jsonCode === 200) {
setPersonalDetails({avatar: response.s3url});
setPersonalDetails({avatar: response.s3url, avatarUploading: false});
}
});
}
Expand Down
16 changes: 15 additions & 1 deletion src/libs/actions/Policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,11 +250,24 @@ function update(policyID, values) {
return;
}

Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, values);
const updatedValues = {...values, ...{isPolicyUpdating: false}};
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, updatedValues);
Navigation.dismissModal();
}).catch(() => {
const errorMessage = translateLocal('workspace.editor.genericFailureMessage');
Growl.error(errorMessage, 5000);
});
}

/**
* Sets local values for the policy
* @param {String} policyID
* @param {Object} values
*/
function updateLocalPolicyValues(policyID, values) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, values);
}

export {
getPolicySummaries,
getPolicyList,
Expand All @@ -263,4 +276,5 @@ export {
create,
uploadAvatar,
update,
updateLocalPolicyValues,
};
1 change: 1 addition & 0 deletions src/pages/settings/Profile/ProfilePage.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class ProfilePage extends Component {
/>
<ScrollView style={styles.flex1} contentContainerStyle={styles.p5}>
<AvatarWithImagePicker
isUploading={this.props.myPersonalDetails.avatarUploading}
avatarURL={this.props.myPersonalDetails.avatar}
onImageSelected={setAvatar}
onImageRemoved={() => deleteAvatar(this.props.myPersonalDetails.login)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const currentUserPersonalDetailsPropsTypes = {
/** Avatar URL of the current user from their personal details */
avatar: PropTypes.string,

/** Flag to set when Avatar uploading */
avatarUploading: PropTypes.bool,

/** Pronouns of the current user from their personal details */
pronouns: PropTypes.string,

Expand Down
20 changes: 18 additions & 2 deletions src/pages/workspace/WorkspaceEditorPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import Button from '../../components/Button';
import Text from '../../components/Text';
import compose from '../../libs/compose';
import {
uploadAvatar, update,
uploadAvatar, update, updateLocalPolicyValues,
} from '../../libs/actions/Policy';
import Icon from '../../components/Icon';
import {Workspace} from '../../components/Icon/Expensicons';
import AvatarWithImagePicker from '../../components/AvatarWithImagePicker';
import defaultTheme from '../../styles/themes/default';
import Growl from '../../libs/Growl';

const propTypes = {
/** List of betas */
Expand Down Expand Up @@ -50,26 +51,35 @@ class WorkspaceEditorPage extends React.Component {
}

onImageSelected(image) {
updateLocalPolicyValues(this.props.policy.id, {isAvatarUploading: true});
this.setState({previewAvatarURL: image.uri});

// Store the upload avatar promise so we can wait for it to finish before updating the policy
this.uploadAvatarPromise = uploadAvatar(image).then(url => new Promise((resolve) => {
updateLocalPolicyValues(this.props.policy.id, {isAvatarUploading: false});
Copy link
Contributor

Choose a reason for hiding this comment

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

We can prevent repeating this line by putting in in an always() right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Can you elaborate this please?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we do a chain like

uploadAvatar(image).then(<stuff>).catch(<stuff>).always(updateLocalPolicyValues(this.props.policy.id, {isAvatarUploading: false});)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Got it. Sure. Will update.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

this.setState({avatarURL: url}, resolve);
}));
})).catch(() => {
Growl.error(this.props.translate('workspace.editor.avatarUploadFailureMessage'));
updateLocalPolicyValues(this.props.policy.id, {isAvatarUploading: false});
});
}

onImageRemoved() {
this.setState({previewAvatarURL: '', avatarURL: ''});
}

submit() {
updateLocalPolicyValues(this.props.policy.id, {isPolicyUpdating: true});

// Wait for the upload avatar promise to finish before updating the policy
this.uploadAvatarPromise.then(() => {
const name = this.state.name.trim();
const avatarURL = this.state.avatarURL;
const policyID = this.props.policy.id;

update(policyID, {name, avatarURL});
}).catch(() => {
updateLocalPolicyValues(this.props.policy.id, {isPolicyUpdating: false});
stitesExpensify marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand All @@ -85,6 +95,9 @@ class WorkspaceEditorPage extends React.Component {
return null;
}

const isButtonDisabled = policy.isAvatarUploading
|| (this.state.avatarURL === this.props.policy.avatarURL
&& this.state.name === this.props.policy.name);
return (
<ScreenWrapper>
<HeaderWithCloseButton
Expand All @@ -95,6 +108,7 @@ class WorkspaceEditorPage extends React.Component {
<View style={[styles.pageWrapper, styles.flex1, styles.pRelative]}>
<View style={[styles.w100, styles.flex1]}>
<AvatarWithImagePicker
isUploading={policy.isAvatarUploading}
avatarURL={this.state.previewAvatarURL}
DefaultAvatar={() => (
<Icon
Expand Down Expand Up @@ -124,6 +138,8 @@ class WorkspaceEditorPage extends React.Component {

<Button
success
isLoading={policy.isPolicyUpdating}
isDisabled={isButtonDisabled}
style={[styles.w100]}
text={this.props.translate('workspace.editor.save')}
onPress={this.submit}
Expand Down