From ec80ff2e06f10e4c122049d7416c2889b277f1ab Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 12 May 2021 02:41:42 +0530 Subject: [PATCH 1/8] new: added sync indicator --- assets/images/sync.svg | 4 + src/components/AvatarWithIndicator.js | 133 ++++++++++++++++++++----- src/components/Icon/Expensicons.js | 2 + src/components/Icon/index.js | 4 +- src/pages/home/sidebar/SidebarLinks.js | 8 ++ src/styles/styles.js | 29 +++--- 6 files changed, 141 insertions(+), 39 deletions(-) create mode 100644 assets/images/sync.svg diff --git a/assets/images/sync.svg b/assets/images/sync.svg new file mode 100644 index 000000000000..e4486116b970 --- /dev/null +++ b/assets/images/sync.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 87b00a1c347d..813111382aac 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -1,8 +1,13 @@ -import React, {memo} from 'react'; -import {View, StyleSheet} from 'react-native'; +import React, {PureComponent} from 'react'; +import { + View, StyleSheet, Animated, Easing, +} from 'react-native'; import PropTypes from 'prop-types'; import Avatar from './Avatar'; +import themeColors from '../styles/themes/default'; import styles from '../styles/styles'; +import Icon from './Icon'; +import {Sync} from './Icon/Expensicons'; const propTypes = { // Is user active? @@ -14,37 +19,115 @@ const propTypes = { // avatar size size: PropTypes.string, + // Whether true, shows sync indicator + isSyncing: PropTypes.bool, }; const defaultProps = { isActive: false, size: 'default', + isSyncing: false, }; -const AvatarWithIndicator = ({ - isActive, - source, - size, -}) => { - const indicatorStyles = [ - size === 'large' ? styles.statusIndicatorLarge : styles.statusIndicator, - isActive ? styles.statusIndicatorOnline : styles.statusIndicatorOffline, - ]; - - return ( - - - - - ); -}; +class AvatarWithIndicator extends PureComponent { + constructor(props) { + super(props); + + this.rotate = new Animated.Value(0); + this.scale = new Animated.Value(1); + this.startSyncIndicator = this.startSyncIndicator.bind(this); + this.stopSyncIndicator = this.stopSyncIndicator.bind(this); + } + + componentDidMount() { + if (this.props.isSyncing) { + this.startSyncIndicator(); + } + } + + componentDidUpdate(prevProps) { + if (!prevProps.isSyncing && this.props.isSyncing) { + this.startSyncIndicator(); + } else if (prevProps.isSyncing && !this.props.isSyncing) { + this.stopSyncIndicator(); + } + } + + componentWillUnmount() { + this.stopSyncIndicator(); + } + + startSyncIndicator() { + Animated.loop(Animated.timing(this.rotate, { + toValue: 1, + duration: 3000, + easing: Easing.linear, + isInteraction: false, + })).start(); + Animated.spring(this.scale, { + toValue: 1.35, + tension: 1, + useNativeDriver: true, + isInteraction: false, + }).start(); + } + + stopSyncIndicator() { + Animated.spring(this.scale, { + toValue: 1, + tension: 1, + useNativeDriver: true, + isInteraction: false, + }).start(() => { + this.rotate.resetAnimation(); + this.scale.resetAnimation(); + this.rotate.setValue(0); + }); + } + + render() { + const indicatorStyles = [ + styles.alignItemsCenter, + styles.justifyContentCenter, + { + transform: [{ + rotate: this.rotate.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '-360deg'], + }), + }, { + scale: this.scale, + }], + }, + this.props.isSyncing ? styles.statusIndicatorSyncing : null, + this.props.size === 'large' ? styles.statusIndicatorLarge : styles.statusIndicator, + this.props.isActive ? styles.statusIndicatorOnline : styles.statusIndicatorOffline, + ]; + + return ( + + + + {this.props.isSyncing && ( + + )} + + + ); + } +} AvatarWithIndicator.defaultProps = defaultProps; AvatarWithIndicator.propTypes = propTypes; AvatarWithIndicator.displayName = 'AvatarWithIndicator'; -export default memo(AvatarWithIndicator); +export default AvatarWithIndicator; diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js index e46fef910b2a..4cd0e1aadf80 100644 --- a/src/components/Icon/Expensicons.js +++ b/src/components/Icon/Expensicons.js @@ -30,6 +30,7 @@ import Camera from '../../../assets/images/camera.svg'; import Gallery from '../../../assets/images/gallery.svg'; import Offline from '../../../assets/images/offline.svg'; import SignOut from '../../../assets/images/sign-out.svg'; +import Sync from '../../../assets/images/sync.svg'; export { ArrowRight, @@ -64,4 +65,5 @@ export { Users, Wallet, SignOut, + Sync, }; diff --git a/src/components/Icon/index.js b/src/components/Icon/index.js index a43d73e69bf4..7f7f471eda04 100644 --- a/src/components/Icon/index.js +++ b/src/components/Icon/index.js @@ -8,10 +8,10 @@ const propTypes = { src: PropTypes.func.isRequired, // The width of the icon. - width: PropTypes.number, + width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // The height of the icon. - height: PropTypes.number, + height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), // The fill color for the icon. // Can be provided in hex, rgb, rgba, or as a valid react-native named color such as 'red' or 'blue'. diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index f31c134ed507..0744145bcf2d 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -71,6 +71,9 @@ const propTypes = { // Whether we have the necessary report data to load the sidebar initialReportDataLoaded: PropTypes.bool, + + // Syncing App Data + isSyncingData: PropTypes.bool, }; const defaultProps = { @@ -84,6 +87,7 @@ const defaultProps = { currentlyViewedReportID: '', priorityMode: CONST.PRIORITY_MODE.DEFAULT, initialReportDataLoaded: false, + isSyncingData: false, }; class SidebarLinks extends React.Component { @@ -143,6 +147,7 @@ class SidebarLinks extends React.Component { @@ -200,5 +205,8 @@ export default compose( initialReportDataLoaded: { key: ONYXKEYS.INITIAL_REPORT_DATA_LOADED, }, + isSyncingData: { + key: ONYXKEYS.IS_LOADING_AFTER_RECONNECT, + }, }), )(SidebarLinks); diff --git a/src/styles/styles.js b/src/styles/styles.js index b29f98292853..5f1422f9fb99 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -457,6 +457,23 @@ const styles = { width: 12, zIndex: 10, }, + + statusIndicatorLarge: { + borderColor: themeColors.componentBG, + borderRadius: 8, + borderWidth: 2, + position: 'absolute', + right: 4, + bottom: 4, + height: 16, + width: 16, + zIndex: 10, + }, + + statusIndicatorSyncing: { + padding: 1, + }, + statusIndicatorOnline: { backgroundColor: themeColors.online, }, @@ -1175,18 +1192,6 @@ const styles = { height: 80, }, - statusIndicatorLarge: { - borderColor: themeColors.componentBG, - borderRadius: 8, - borderWidth: 2, - position: 'absolute', - right: 4, - bottom: 4, - height: 16, - width: 16, - zIndex: 10, - }, - displayName: { fontSize: variables.fontSizeLarge, fontFamily: fontFamily.GTA_BOLD, From e37ee152ff87a5aab92a4e6ed81f72e3e4f906b1 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 12 May 2021 03:13:03 +0530 Subject: [PATCH 2/8] animation on native --- src/components/AvatarWithIndicator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 813111382aac..62f8fb3b671d 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -63,12 +63,13 @@ class AvatarWithIndicator extends PureComponent { duration: 3000, easing: Easing.linear, isInteraction: false, + useNativeDriver: true, })).start(); Animated.spring(this.scale, { toValue: 1.35, tension: 1, - useNativeDriver: true, isInteraction: false, + useNativeDriver: true, }).start(); } @@ -76,8 +77,8 @@ class AvatarWithIndicator extends PureComponent { Animated.spring(this.scale, { toValue: 1, tension: 1, - useNativeDriver: true, isInteraction: false, + useNativeDriver: true, }).start(() => { this.rotate.resetAnimation(); this.scale.resetAnimation(); From ae3858c9b624a85443bb7524407ee68f91a71dce Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 12 May 2021 03:24:17 +0530 Subject: [PATCH 3/8] typo --- src/components/AvatarWithIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 62f8fb3b671d..293e12ad84d7 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -19,7 +19,7 @@ const propTypes = { // avatar size size: PropTypes.string, - // Whether true, shows sync indicator + // When true, shows sync indicator isSyncing: PropTypes.bool, }; From a3baacd163cf9e9b90b2e3baa1ccf1c76086c816 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Fri, 14 May 2021 05:03:10 +0530 Subject: [PATCH 4/8] Update src/pages/home/sidebar/SidebarLinks.js Co-authored-by: Amal Nazeem --- src/pages/home/sidebar/SidebarLinks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 0744145bcf2d..b6f339228e25 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -72,7 +72,7 @@ const propTypes = { // Whether we have the necessary report data to load the sidebar initialReportDataLoaded: PropTypes.bool, - // Syncing App Data + // Whether we are syncing app data isSyncingData: PropTypes.bool, }; From 78a084b7e7d4337420c51b5312a05144ae6c6a86 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Fri, 14 May 2021 05:03:29 +0530 Subject: [PATCH 5/8] Update src/components/AvatarWithIndicator.js Co-authored-by: Amal Nazeem --- src/components/AvatarWithIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 293e12ad84d7..daa6957d65d4 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -19,7 +19,7 @@ const propTypes = { // avatar size size: PropTypes.string, - // When true, shows sync indicator + // Whether we show the sync indicator isSyncing: PropTypes.bool, }; From fa3ae71873ed51f3842820530176cef1d3db6cfb Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Sat, 15 May 2021 02:14:46 +0530 Subject: [PATCH 6/8] fix: animation issue with indicator --- src/components/AvatarWithIndicator.js | 50 ++++++++++++++-------- src/styles/getAvatarWithIndicatorStyles.js | 23 ++++++++++ src/styles/styles.js | 5 ++- 3 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 src/styles/getAvatarWithIndicatorStyles.js diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 293e12ad84d7..1b45465c4418 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -8,6 +8,7 @@ import themeColors from '../styles/themes/default'; import styles from '../styles/styles'; import Icon from './Icon'; import {Sync} from './Icon/Expensicons'; +import {getSyncingStyles} from '../styles/getAvatarWithIndicatorStyles'; const propTypes = { // Is user active? @@ -35,6 +36,7 @@ class AvatarWithIndicator extends PureComponent { this.rotate = new Animated.Value(0); this.scale = new Animated.Value(1); + this.startRotation = this.startRotation.bind(this); this.startSyncIndicator = this.startSyncIndicator.bind(this); this.stopSyncIndicator = this.stopSyncIndicator.bind(this); } @@ -57,22 +59,46 @@ class AvatarWithIndicator extends PureComponent { this.stopSyncIndicator(); } - startSyncIndicator() { - Animated.loop(Animated.timing(this.rotate, { + /** + * We need to manually loop the animations as `useNativeDriver` does not work well with Animated.loop. + * + * @memberof AvatarWithIndicator + */ + startRotation() { + this.rotate.setValue(0); + Animated.timing(this.rotate, { toValue: 1, duration: 3000, easing: Easing.linear, isInteraction: false, useNativeDriver: true, - })).start(); + }).start(({finished}) => { + if (finished) { + this.startRotation(); + } + }); + } + + /** + * Start Animation for Indicator + * + * @memberof AvatarWithIndicator + */ + startSyncIndicator() { + this.startRotation(); Animated.spring(this.scale, { - toValue: 1.35, + toValue: 1.666, tension: 1, isInteraction: false, useNativeDriver: true, }).start(); } + /** + * Stop Animation for Indicator + * + * @memberof AvatarWithIndicator + */ stopSyncIndicator() { Animated.spring(this.scale, { toValue: 1, @@ -90,19 +116,9 @@ class AvatarWithIndicator extends PureComponent { const indicatorStyles = [ styles.alignItemsCenter, styles.justifyContentCenter, - { - transform: [{ - rotate: this.rotate.interpolate({ - inputRange: [0, 1], - outputRange: ['0deg', '-360deg'], - }), - }, { - scale: this.scale, - }], - }, - this.props.isSyncing ? styles.statusIndicatorSyncing : null, this.props.size === 'large' ? styles.statusIndicatorLarge : styles.statusIndicator, this.props.isActive ? styles.statusIndicatorOnline : styles.statusIndicatorOffline, + getSyncingStyles(this.rotate, this.scale), ]; return ( @@ -118,8 +134,8 @@ class AvatarWithIndicator extends PureComponent { )} diff --git a/src/styles/getAvatarWithIndicatorStyles.js b/src/styles/getAvatarWithIndicatorStyles.js new file mode 100644 index 000000000000..7b9a97568b15 --- /dev/null +++ b/src/styles/getAvatarWithIndicatorStyles.js @@ -0,0 +1,23 @@ +/** + * Get Indicator Styles while animating + * + * @param {Object} rotate + * @param {Object} scale + * @returns {Object} + */ +function getSyncingStyles(rotate, scale) { + return { + transform: [{ + rotate: rotate.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '-360deg'], + }), + }, + { + scale, + }], + }; +} + +// eslint-disable-next-line import/prefer-default-export +export {getSyncingStyles}; diff --git a/src/styles/styles.js b/src/styles/styles.js index 0a9224e36936..16c0bb704e32 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -471,7 +471,10 @@ const styles = { }, statusIndicatorSyncing: { - padding: 1, + // padding: 1, + // borderRadius: 8, + // right: -2, + // bottom: -2, }, statusIndicatorOnline: { From 293375bc7d0bc80151065f4a2416b9353461fdbe Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Mon, 17 May 2021 02:41:23 +0530 Subject: [PATCH 7/8] fix: sync icon is visible after user is offline --- src/pages/home/sidebar/SidebarLinks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index b9aca6fd4127..30175cf49d98 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -147,7 +147,7 @@ class SidebarLinks extends React.Component { From 8aca3f289a94218ea0c0f0fc0a24749b15fc87bf Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 19 May 2021 02:31:06 +0530 Subject: [PATCH 8/8] chore: animation timing --- src/components/AvatarWithIndicator.js | 2 +- src/styles/styles.js | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index dcb5b8a72143..50cda0e56bb6 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -68,7 +68,7 @@ class AvatarWithIndicator extends PureComponent { this.rotate.setValue(0); Animated.timing(this.rotate, { toValue: 1, - duration: 3000, + duration: 2000, easing: Easing.linear, isInteraction: false, useNativeDriver: true, diff --git a/src/styles/styles.js b/src/styles/styles.js index 16c0bb704e32..30de1fa7d443 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -470,13 +470,6 @@ const styles = { zIndex: 10, }, - statusIndicatorSyncing: { - // padding: 1, - // borderRadius: 8, - // right: -2, - // bottom: -2, - }, - statusIndicatorOnline: { backgroundColor: themeColors.online, },