Skip to content
142 changes: 41 additions & 101 deletions src/components/AvatarWithIndicator.js
Original file line number Diff line number Diff line change
@@ -1,126 +1,66 @@
import React, {PureComponent} from 'react';
import {
View, StyleSheet, Animated,
} from 'react-native';
import _ from 'underscore';
import React from 'react';
import {StyleSheet, View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import Avatar from './Avatar';
import themeColors from '../styles/themes/default';
import styles from '../styles/styles';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import SpinningIndicatorAnimation from '../styles/animation/SpinningIndicatorAnimation';
import Tooltip from './Tooltip';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import ONYXKEYS from '../ONYXKEYS';
import policyMemberPropType from '../pages/policyMemberPropType';
import * as Policy from '../libs/actions/Policy';

const propTypes = {
/** Is user active? */
isActive: PropTypes.bool,

/** URL for the avatar */
source: PropTypes.string.isRequired,

/** Avatar size */
size: PropTypes.string,

// Whether we show the sync indicator
isSyncing: PropTypes.bool,

/** To show a tooltip on hover */
tooltipText: PropTypes.string,

...withLocalizePropTypes,
/** The employee list of all policies (coming from Onyx) */
policiesMemberList: PropTypes.objectOf(policyMemberPropType),
};

const defaultProps = {
isActive: false,
size: 'default',
isSyncing: false,
tooltipText: '',
policiesMemberList: {},
};

class AvatarWithIndicator extends PureComponent {
constructor(props) {
super(props);

this.animation = new SpinningIndicatorAnimation();
}

componentDidMount() {
if (!this.props.isSyncing) {
return;
}

this.animation.start();
}

componentDidUpdate(prevProps) {
if (!prevProps.isSyncing && this.props.isSyncing) {
this.animation.start();
} else if (prevProps.isSyncing && !this.props.isSyncing) {
this.animation.stop();
}
}

componentWillUnmount() {
this.animation.stop();
}

/**
* Returns user status as text
*
* @returns {String}
*/
userStatus() {
if (this.props.isSyncing) {
return this.props.translate('profilePage.syncing');
}

if (this.props.isActive) {
return this.props.translate('profilePage.online');
}

if (!this.props.isActive) {
return this.props.translate('profilePage.offline');
}
}

render() {
const indicatorStyles = [
styles.alignItemsCenter,
styles.justifyContentCenter,
this.props.size === 'large' ? styles.statusIndicatorLarge : styles.statusIndicator,
this.props.isActive ? styles.statusIndicatorOnline : styles.statusIndicatorOffline,
this.animation.getSyncingStyles(),
];

return (
<View
style={[this.props.size === 'large' ? styles.avatarLarge : styles.sidebarAvatar]}
>
<Tooltip text={this.props.tooltipText}>
<Avatar
imageStyles={[this.props.size === 'large' ? styles.avatarLarge : null]}
source={this.props.source}
size={this.props.size}
/>
</Tooltip>
<Tooltip text={this.userStatus()} absolute>
<Animated.View style={StyleSheet.flatten(indicatorStyles)}>
{this.props.isSyncing && (
<Icon
src={Expensicons.Sync}
fill={themeColors.textReversed}
width={6}
height={6}
/>
)}
Comment on lines -109 to -116

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are we purposely removing the sync icon?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes

</Animated.View>
</Tooltip>
</View>
);
}
}
const AvatarWithIndicator = (props) => {
const isLarge = props.size === 'large';
const indicatorStyles = [
styles.alignItemsCenter,
styles.justifyContentCenter,
isLarge ? styles.statusIndicatorLarge : styles.statusIndicator,
];

const hasPolicyMemberError = _.some(props.policiesMemberList, policyMembers => Policy.hasPolicyMemberError(policyMembers));
return (
<View style={[isLarge ? styles.avatarLarge : styles.sidebarAvatar]}>
<Tooltip text={props.tooltipText}>
<Avatar
imageStyles={[isLarge ? styles.avatarLarge : null]}
source={props.source}
size={props.size}
/>
{hasPolicyMemberError && (
<View style={StyleSheet.flatten(indicatorStyles)} />
)}
</Tooltip>
</View>
);
};

AvatarWithIndicator.defaultProps = defaultProps;
AvatarWithIndicator.propTypes = propTypes;
export default withLocalize(AvatarWithIndicator);
AvatarWithIndicator.displayName = 'AvatarWithIndicator';

export default withOnyx({
policiesMemberList: {
key: ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST,
},
})(AvatarWithIndicator);
1 change: 1 addition & 0 deletions src/components/OfflineWithFeedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ const OfflineWithFeedback = (props) => {

OfflineWithFeedback.propTypes = propTypes;
OfflineWithFeedback.defaultProps = defaultProps;
OfflineWithFeedback.displayName = 'OfflineWithFeedback';

export default compose(
withLocalize,
Expand Down
31 changes: 30 additions & 1 deletion src/libs/actions/Policy.js
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,34 @@ function subscribeToPolicyEvents() {
}

/**
* Checks if we have any errors stored within the POLICY_MEMBER_LIST. Determines whether we should show a red brick road error or not
* Removes an error after trying to delete a member
*
* @param {String} policyID
* @param {String} memberEmail
*/
function clearDeleteMemberError(policyID, memberEmail) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, {
[memberEmail]: {
pendingAction: null,
errors: null,
},
});
}

/**
* Removes an error after trying to add a member
*
* @param {String} policyID
* @param {String} memberEmail
*/
function clearAddMemberError(policyID, memberEmail) {
Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, {
[memberEmail]: null,
});
}

/**
* Checks if we have any errors stored within the POLICY_MEMBER_LIST. Determines whether we should show a red brick road error or not
* Data structure: {email: {role:'bla', errors: []}, email2: {role:'bla', errors: [{1231312313: 'Unable to do X'}]}, ...}
* @param {Object} policyMemberList
* @returns {Boolean}
Expand All @@ -567,5 +594,7 @@ export {
setCustomUnitRate,
updateLastAccessedWorkspace,
subscribeToPolicyEvents,
clearDeleteMemberError,
clearAddMemberError,
hasPolicyMemberError,
};
3 changes: 3 additions & 0 deletions src/pages/policyMemberPropType.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export default PropTypes.shape({
* {<timestamp>: 'error message', <timestamp2>: 'error message 2'}
*/
errors: PropTypes.objectOf(PropTypes.string),

/** Is this action pending? */
pendingAction: PropTypes.string,
});
22 changes: 9 additions & 13 deletions src/pages/settings/InitialSettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import themeColors from '../../styles/themes/default';
import Text from '../../components/Text';
import * as Session from '../../libs/actions/Session';
import ONYXKEYS from '../../ONYXKEYS';
import AvatarWithIndicator from '../../components/AvatarWithIndicator';
import Tooltip from '../../components/Tooltip';
import Avatar from '../../components/Avatar';
import HeaderWithCloseButton from '../../components/HeaderWithCloseButton';
import Navigation from '../../libs/Navigation/Navigation';
import * as Expensicons from '../../components/Icon/Expensicons';
Expand All @@ -21,8 +22,6 @@ import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize
import compose from '../../libs/compose';
import CONST from '../../CONST';
import Permissions from '../../libs/Permissions';
import networkPropTypes from '../../components/networkPropTypes';
import {withNetwork} from '../../components/OnyxProvider';
import * as App from '../../libs/actions/App';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../components/withCurrentUserPersonalDetails';
import * as Policy from '../../libs/actions/Policy';
Expand All @@ -31,9 +30,6 @@ import policyMemberPropType from '../policyMemberPropType';
const propTypes = {
/* Onyx Props */

/** Information about the network */
network: networkPropTypes.isRequired,

/** The session of the logged in person */
session: PropTypes.shape({
/** Email of the logged in person */
Expand Down Expand Up @@ -156,12 +152,13 @@ const InitialSettingsPage = (props) => {
<View style={styles.w100}>
<View style={styles.pageWrapper}>
<Pressable style={[styles.mb3]} onPress={openProfileSettings}>
<AvatarWithIndicator
size={CONST.AVATAR_SIZE.LARGE}
source={props.currentUserPersonalDetails.avatar}
isActive={props.network.isOffline === false}
tooltipText={props.currentUserPersonalDetails.displayName}
/>
<Tooltip text={props.currentUserPersonalDetails.displayName}>
<Avatar
imageStyles={[styles.avatarLarge]}
source={props.currentUserPersonalDetails.avatar}
size={CONST.AVATAR_SIZE.LARGE}
/>
</Tooltip>
</Pressable>

<Pressable style={[styles.mt1, styles.mw100]} onPress={openProfileSettings}>
Expand Down Expand Up @@ -211,7 +208,6 @@ InitialSettingsPage.displayName = 'InitialSettingsPage';

export default compose(
withLocalize,
withNetwork(),
withCurrentUserPersonalDetails,
withOnyx({
session: {
Expand Down
20 changes: 19 additions & 1 deletion src/pages/workspace/WorkspaceInitialPage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import _ from 'underscore';
import React from 'react';
import {View, ScrollView, Pressable} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import PropTypes from 'prop-types';
import Navigation from '../../libs/Navigation/Navigation';
import ROUTES from '../../ROUTES';
import styles from '../../styles/styles';
Expand All @@ -20,13 +22,21 @@ import FullScreenLoadingIndicator from '../../components/FullscreenLoadingIndica
import withFullPolicy, {fullPolicyPropTypes, fullPolicyDefaultProps} from './withFullPolicy';
import * as PolicyActions from '../../libs/actions/Policy';
import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
import policyMemberPropType from '../policyMemberPropType';

const propTypes = {
...fullPolicyPropTypes,
...withLocalizePropTypes,

/** The employee list of this policy (coming from Onyx) */
policyMemberList: PropTypes.objectOf(policyMemberPropType),
};

const defaultProps = fullPolicyDefaultProps;
const defaultProps = {
...fullPolicyDefaultProps,
policyMemberList: {},
};

class WorkspaceInitialPage extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -70,6 +80,7 @@ class WorkspaceInitialPage extends React.Component {
return <FullScreenLoadingIndicator />;
}

const hasMembersError = PolicyActions.hasPolicyMemberError(this.props.policyMemberList);
const menuItems = [
{
translationKey: 'workspace.common.settings',
Expand Down Expand Up @@ -105,6 +116,7 @@ class WorkspaceInitialPage extends React.Component {
translationKey: 'workspace.common.members',
icon: Expensicons.Users,
action: () => Navigation.navigate(ROUTES.getWorkspaceMembersRoute(policy.id)),
error: hasMembersError,
},
{
translationKey: 'workspace.common.bankAccount',
Expand Down Expand Up @@ -202,6 +214,7 @@ class WorkspaceInitialPage extends React.Component {
iconRight={item.iconRight}
onPress={() => item.action()}
shouldShowRightIcon
brickRoadIndicator={item.error ? 'error' : null}
/>
))}
</View>
Expand All @@ -228,4 +241,9 @@ WorkspaceInitialPage.displayName = 'WorkspaceInitialPage';
export default compose(
withLocalize,
withFullPolicy,
withOnyx({
policyMemberList: {
key: ({policy}) => `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policy.id}`,
},
}),
)(WorkspaceInitialPage);
Loading