From 8c09ebea2b60e45a2f93bc6deb6cc97d92df3093 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 19 Mar 2024 04:06:54 +0500 Subject: [PATCH 01/19] add temp focus --- src/components/MenuItem.tsx | 31 +++++++++++++++++++- src/pages/workspace/WorkspaceInitialPage.tsx | 4 +++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 6835bcf3f5fc..7f4ac0b11e64 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -148,6 +148,12 @@ type MenuItemBaseProps = { /** Whether item is focused or active */ focused?: boolean; + /** Temporarily focus the item indicating an action (like setting enable) occured */ + shouldFocusTemporarily?: boolean; + + /** The duration to focus the item temporarily */ + temporaryFocusDuration?: number; + /** Should we disable this menu item? */ disabled?: boolean; @@ -276,6 +282,8 @@ function MenuItem( success = false, focused = false, disabled = false, + shouldFocusTemporarily = false, + temporaryFocusDuration = 3000, title, subtitle, shouldShowBasicTitle, @@ -315,7 +323,9 @@ function MenuItem( const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = useState(''); const titleRef = useRef(''); + const tempFocusTimeoutRef = useRef(); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; + const [temporaryilyFocused, setTemporaryilyFocused] = useState(false); const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; @@ -342,6 +352,25 @@ function MenuItem( isDeleted ? styles.offlineFeedback.deleted : {}, ]); + useEffect(() => { + if (!shouldFocusTemporarily) { + return; + } + setTemporaryilyFocused(true); + tempFocusTimeoutRef.current = setTimeout(() => setTemporaryilyFocused(false), temporaryFocusDuration); + }, [shouldFocusTemporarily, temporaryFocusDuration]); + + useEffect( + () => () => { + if (!tempFocusTimeoutRef.current) { + return; + } + + clearTimeout(tempFocusTimeoutRef.current); + }, + [], + ); + useEffect(() => { if (!title || (titleRef.current.length && titleRef.current === title) || !shouldParseTitle) { return; @@ -417,7 +446,7 @@ function MenuItem( errorText ? styles.pb5 : {}, combinedStyle, !interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(temporaryilyFocused || focused || isHovered, pressed, success, disabled, interactive), true), !focused && (isHovered || pressed) && hoverAndPressStyle, ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 9a56a2da0314..fc1074260b0f 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -226,6 +226,9 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r const prevPolicy = usePrevious(policy); + const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); + const enabledItems = protectedCollectPolicyMenuItems.filter((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.translationKey === prevItem.translationKey)); + // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = isEmptyObject(policy) || @@ -287,6 +290,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r brickRoadIndicator={item.brickRoadIndicator} wrapperStyle={styles.sectionMenuItem} focused={!!(item.routeName && activeRoute?.startsWith(item.routeName))} + shouldFocusTemporarily={enabledItems.length === 1 && enabledItems[0].translationKey === item.translationKey} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu /> From 086b648227c2fc9ed4d770c2023a69ba963ee7bb Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 21 Mar 2024 00:53:27 +0500 Subject: [PATCH 02/19] handle comments --- src/ROUTES.ts | 2 +- src/components/MenuItem.tsx | 42 ++++++-------------- src/hooks/useHighlightToggle.ts | 27 +++++++++++++ src/libs/Navigation/types.ts | 1 + src/libs/actions/Policy.ts | 15 +++---- src/pages/workspace/WorkspaceInitialPage.tsx | 10 +++-- 6 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 src/hooks/useHighlightToggle.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 5eda71fb34d4..b1ae1d77a483 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -438,7 +438,7 @@ const ROUTES = { WORKSPACE_NEW_ROOM: 'workspace/new-room', WORKSPACE_INITIAL: { route: 'settings/workspaces/:policyID', - getRoute: (policyID: string) => `settings/workspaces/${policyID}` as const, + getRoute: (policyID: string, featureRouteName?: string) => `settings/workspaces/${policyID}${featureRouteName ? `?enabledFeatureRouteName=${featureRouteName}` : ''}` as const, }, WORKSPACE_INVITE: { route: 'settings/workspaces/:policyID/invite', diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 7f4ac0b11e64..4e117e05e460 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -6,6 +6,7 @@ import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react import {View} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; +import useHighlightToggle from '@hooks/useHighlightToggle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -149,10 +150,10 @@ type MenuItemBaseProps = { focused?: boolean; /** Temporarily focus the item indicating an action (like setting enable) occured */ - shouldFocusTemporarily?: boolean; + shouldHighlight?: boolean; - /** The duration to focus the item temporarily */ - temporaryFocusDuration?: number; + /** The duration to highlight the item */ + highlightDuration?: number; /** Should we disable this menu item? */ disabled?: boolean; @@ -282,8 +283,8 @@ function MenuItem( success = false, focused = false, disabled = false, - shouldFocusTemporarily = false, - temporaryFocusDuration = 3000, + shouldHighlight = false, + highlightDuration = 2000, title, subtitle, shouldShowBasicTitle, @@ -323,10 +324,8 @@ function MenuItem( const {isSmallScreenWidth} = useWindowDimensions(); const [html, setHtml] = useState(''); const titleRef = useRef(''); - const tempFocusTimeoutRef = useRef(); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; - const [temporaryilyFocused, setTemporaryilyFocused] = useState(false); - + const highlighted = useHighlightToggle(shouldHighlight, highlightDuration); const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; @@ -352,25 +351,6 @@ function MenuItem( isDeleted ? styles.offlineFeedback.deleted : {}, ]); - useEffect(() => { - if (!shouldFocusTemporarily) { - return; - } - setTemporaryilyFocused(true); - tempFocusTimeoutRef.current = setTimeout(() => setTemporaryilyFocused(false), temporaryFocusDuration); - }, [shouldFocusTemporarily, temporaryFocusDuration]); - - useEffect( - () => () => { - if (!tempFocusTimeoutRef.current) { - return; - } - - clearTimeout(tempFocusTimeoutRef.current); - }, - [], - ); - useEffect(() => { if (!title || (titleRef.current.length && titleRef.current === title) || !shouldParseTitle) { return; @@ -446,7 +426,7 @@ function MenuItem( errorText ? styles.pb5 : {}, combinedStyle, !interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(temporaryilyFocused || focused || isHovered, pressed, success, disabled, interactive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(highlighted || focused || isHovered, pressed, success, disabled, interactive), true), !focused && (isHovered || pressed) && hoverAndPressStyle, ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, @@ -496,7 +476,11 @@ function MenuItem( displayInDefaultIconColor ? undefined : iconFill ?? - StyleUtils.getIconFillColor(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true, isPaneMenu) + StyleUtils.getIconFillColor( + getButtonState(highlighted || focused || isHovered, pressed, success, disabled, interactive), + true, + isPaneMenu, + ) } /> )} diff --git a/src/hooks/useHighlightToggle.ts b/src/hooks/useHighlightToggle.ts new file mode 100644 index 000000000000..40f1c26739f7 --- /dev/null +++ b/src/hooks/useHighlightToggle.ts @@ -0,0 +1,27 @@ +import {useEffect, useRef, useState} from 'react'; +import CONST from '@src/CONST'; + +/** + * Returns a toggle that gets un-toggled after the specified time delay has elapsed. + * This is used for toggling a highlight effect on a component. + */ +export default function useHighlightToggle(shouldToggle: boolean, delay: number = CONST.ANIMATED_TRANSITION) { + const toggleTimeoutRef = useRef(); + const [toggle, setToggle] = useState(false); + + useEffect(() => { + if (!shouldToggle) { + return; + } + + setToggle(true); + + toggleTimeoutRef.current = setTimeout(() => { + setToggle(false); + }, delay); + }, [shouldToggle, delay]); + + useEffect(() => () => toggleTimeoutRef.current && clearTimeout(toggleTimeoutRef.current), []); + + return toggle; +} diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 45e31e821225..d39dcd7a4de4 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -580,6 +580,7 @@ type WorkspacesCentralPaneNavigatorParamList = { type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.INITIAL]: { policyID: string; + enabledFeatureRouteName?: string; }; [SCREENS.WORKSPACES_CENTRAL_PANE]: NavigatorScreenParams; }; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 816d26b9c9e2..10d283ccec4c 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -63,6 +63,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type { InvitedEmailsToAccountIDs, PersonalDetailsList, @@ -3393,11 +3394,11 @@ function openPolicyDistanceRatesPage(policyID?: string) { API.read(READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE, params); } -function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { +function navigateWhenEnableFeature(policyID: string, featureRouteName: string, featureRoute: Route) { const isNarrowLayout = getIsNarrowLayout(); if (isNarrowLayout) { - Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policyID)); + Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, featureRouteName)); return; } @@ -3448,7 +3449,7 @@ function enablePolicyCategories(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); + navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.CATEGORIES, ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); } } @@ -3540,7 +3541,7 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)); + navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.DISTANCE_RATES, ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)); } } @@ -3632,7 +3633,7 @@ function enablePolicyTags(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAGS.getRoute(policyID)); + navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.TAGS, ROUTES.WORKSPACE_TAGS.getRoute(policyID)); } } @@ -3684,7 +3685,7 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAXES.getRoute(policyID)); + navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.TAXES, ROUTES.WORKSPACE_TAXES.getRoute(policyID)); } } @@ -3775,7 +3776,7 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)); + navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.WORKFLOWS, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)); } } diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index fc1074260b0f..939310fcaa39 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,4 +1,5 @@ -import {useNavigationState} from '@react-navigation/native'; +import {useNavigationState, useRoute} from '@react-navigation/native'; +import type {RouteProp} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; @@ -225,9 +226,10 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r ]; const prevPolicy = usePrevious(policy); + const route = useRoute>(); + const enabledFeatureRouteName = route.params?.enabledFeatureRouteName ?? ''; - const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); - const enabledItems = protectedCollectPolicyMenuItems.filter((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.translationKey === prevItem.translationKey)); + const enabledItem = protectedCollectPolicyMenuItems.find((item) => item.routeName === enabledFeatureRouteName); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = @@ -290,7 +292,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r brickRoadIndicator={item.brickRoadIndicator} wrapperStyle={styles.sectionMenuItem} focused={!!(item.routeName && activeRoute?.startsWith(item.routeName))} - shouldFocusTemporarily={enabledItems.length === 1 && enabledItems[0].translationKey === item.translationKey} + shouldHighlight={enabledItem?.translationKey === item.translationKey} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu /> From 66dedbbab314790d58f177b7d0200be423fa5a81 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Sat, 23 Mar 2024 02:37:39 +0500 Subject: [PATCH 03/19] add animations as design wanted --- src/components/MenuItem.tsx | 21 +- .../index.tsx | 3 +- src/hooks/useAnimatedHighlightStyle.ts | 36 +++ src/hooks/useHighlightToggle.ts | 27 --- src/libs/actions/Policy.ts | 5 +- src/pages/workspace/WorkspaceInitialPage.tsx | 208 +++++++++++------- 6 files changed, 175 insertions(+), 125 deletions(-) create mode 100644 src/hooks/useAnimatedHighlightStyle.ts delete mode 100644 src/hooks/useHighlightToggle.ts diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 4e117e05e460..bb5588b62fd8 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -6,7 +6,6 @@ import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react import {View} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; -import useHighlightToggle from '@hooks/useHighlightToggle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -149,11 +148,8 @@ type MenuItemBaseProps = { /** Whether item is focused or active */ focused?: boolean; - /** Temporarily focus the item indicating an action (like setting enable) occured */ - shouldHighlight?: boolean; - - /** The duration to highlight the item */ - highlightDuration?: number; + /** Style for the highlighted state */ + highlightStyle?: StyleProp>; /** Should we disable this menu item? */ disabled?: boolean; @@ -283,8 +279,7 @@ function MenuItem( success = false, focused = false, disabled = false, - shouldHighlight = false, - highlightDuration = 2000, + highlightStyle = {}, title, subtitle, shouldShowBasicTitle, @@ -325,7 +320,6 @@ function MenuItem( const [html, setHtml] = useState(''); const titleRef = useRef(''); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; - const highlighted = useHighlightToggle(shouldHighlight, highlightDuration); const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; @@ -420,13 +414,14 @@ function MenuItem( onPressIn={() => shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={onSecondaryInteraction} + wrapperStyle={highlightStyle} style={({pressed}) => [ containerStyle, errorText ? styles.pb5 : {}, combinedStyle, !interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(highlighted || focused || isHovered, pressed, success, disabled, interactive), true), + StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true), !focused && (isHovered || pressed) && hoverAndPressStyle, ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, @@ -476,11 +471,7 @@ function MenuItem( displayInDefaultIconColor ? undefined : iconFill ?? - StyleUtils.getIconFillColor( - getButtonState(highlighted || focused || isHovered, pressed, success, disabled, interactive), - true, - isPaneMenu, - ) + StyleUtils.getIconFillColor(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true, isPaneMenu) } /> )} diff --git a/src/components/PressableWithSecondaryInteraction/index.tsx b/src/components/PressableWithSecondaryInteraction/index.tsx index 5e2de765f733..cbcf8523d9a4 100644 --- a/src/components/PressableWithSecondaryInteraction/index.tsx +++ b/src/components/PressableWithSecondaryInteraction/index.tsx @@ -13,6 +13,7 @@ function PressableWithSecondaryInteraction( children, inline = false, style, + wrapperStyle, enableLongPressWithHover = false, withoutFocusOnSecondaryInteraction = false, needsOffscreenAlphaCompositing = false, @@ -96,7 +97,7 @@ function PressableWithSecondaryInteraction( // ESLint is disabled here to propagate all the props, enhancing PressableWithSecondaryInteraction's versatility across different use cases. // eslint-disable-next-line react/jsx-props-no-spreading {...rest} - wrapperStyle={StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle)} + wrapperStyle={[StyleUtils.combineStyles(DeviceCapabilities.canUseTouchScreen() ? [styles.userSelectNone, styles.noSelect] : [], inlineStyle), wrapperStyle]} onLongPress={onSecondaryInteraction ? executeSecondaryInteraction : undefined} pressDimmingValue={activeOpacity} ref={pressableRef} diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts new file mode 100644 index 000000000000..4996e0bdb13e --- /dev/null +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -0,0 +1,36 @@ +import {useFocusEffect} from '@react-navigation/native'; +import React from 'react'; +import {InteractionManager} from 'react-native'; +import {interpolateColor, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; +import CONST from '@src/CONST'; +import useTheme from './useTheme'; + +/** + * Returns a highlight style that interpolates the colour giving a fading effect. + */ +export default function useAnimatedHighlightStyle(shouldHighlight: boolean, highlightDuration: number = CONST.ANIMATED_TRANSITION, delay = 100) { + const highlightProgress = useSharedValue(0); + const theme = useTheme(); + + const highlightBackgroundStyle = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor(highlightProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), + })); + + useFocusEffect( + React.useCallback(() => { + if (!shouldHighlight) { + return; + } + InteractionManager.runAfterInteractions(() => { + highlightProgress.value = withSequence( + withDelay(delay, withTiming(0)), + withTiming(1, {duration: highlightDuration}), + withDelay(delay, withTiming(1)), + withTiming(0, {duration: highlightDuration}), + ); + }); + }, [shouldHighlight, highlightDuration, delay, highlightProgress]), + ); + + return highlightBackgroundStyle; +} diff --git a/src/hooks/useHighlightToggle.ts b/src/hooks/useHighlightToggle.ts deleted file mode 100644 index 40f1c26739f7..000000000000 --- a/src/hooks/useHighlightToggle.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {useEffect, useRef, useState} from 'react'; -import CONST from '@src/CONST'; - -/** - * Returns a toggle that gets un-toggled after the specified time delay has elapsed. - * This is used for toggling a highlight effect on a component. - */ -export default function useHighlightToggle(shouldToggle: boolean, delay: number = CONST.ANIMATED_TRANSITION) { - const toggleTimeoutRef = useRef(); - const [toggle, setToggle] = useState(false); - - useEffect(() => { - if (!shouldToggle) { - return; - } - - setToggle(true); - - toggleTimeoutRef.current = setTimeout(() => { - setToggle(false); - }, delay); - }, [shouldToggle, delay]); - - useEffect(() => () => toggleTimeoutRef.current && clearTimeout(toggleTimeoutRef.current), []); - - return toggle; -} diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 10d283ccec4c..5e20fa1840c9 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3396,9 +3396,10 @@ function openPolicyDistanceRatesPage(policyID?: string) { function navigateWhenEnableFeature(policyID: string, featureRouteName: string, featureRoute: Route) { const isNarrowLayout = getIsNarrowLayout(); - if (isNarrowLayout) { - Navigation.goBack(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, featureRouteName)); + setTimeout(() => { + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, featureRouteName)); + }, 1000); return; } diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 939310fcaa39..a3933e997bab 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,10 +1,11 @@ -import {useNavigationState, useRoute} from '@react-navigation/native'; +import {useFocusEffect, useNavigationState, useRoute} from '@react-navigation/native'; import type {RouteProp} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import Animated, {FadeInUp} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; @@ -14,6 +15,7 @@ import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useSingleExecution from '@hooks/useSingleExecution'; @@ -43,7 +45,7 @@ type WorkspaceMenuItem = { icon: IconAsset; action: () => void; brickRoadIndicator?: ValueOf; - routeName?: ValueOf; + name?: ValueOf; }; type WorkspaceInitialPageOnyxProps = { @@ -70,6 +72,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r const {singleExecution, isExecuting} = useSingleExecution(); const activeRoute = useNavigationState(getTopmostWorkspacesCentralPaneName); const {translate} = useLocalize(); + const [menuItems, setMenuItems] = useState>>([]); const policyID = policy?.id ?? ''; const policyName = policy?.name ?? ''; @@ -107,36 +110,50 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); const isFreeGroupPolicy = PolicyUtils.isFreeGroupPolicy(policy); - const protectedFreePolicyMenuItems: WorkspaceMenuItem[] = [ + const allMenuItems: WorkspaceMenuItem[] = [ + { + translationKey: 'workspace.common.profile', + icon: Expensicons.Home, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))), + brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + name: SCREENS.WORKSPACE.PROFILE, + }, + { + translationKey: 'workspace.common.members', + icon: Expensicons.Users, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)))), + brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + name: SCREENS.WORKSPACE.MEMBERS, + }, { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.CARD, + name: SCREENS.WORKSPACE.CARD, }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.REIMBURSE, + name: SCREENS.WORKSPACE.REIMBURSE, }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.BILLS, + name: SCREENS.WORKSPACE.BILLS, }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.INVOICES, + name: SCREENS.WORKSPACE.INVOICES, }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.TRAVEL, + name: SCREENS.WORKSPACE.TRAVEL, }, { translationKey: 'workspace.common.bankAccount', @@ -147,89 +164,112 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r : setIsCurrencyModalOpen(true), brickRoadIndicator: !isEmptyObject(reimbursementAccount?.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, - ]; - - const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = []; - - if (policy?.areDistanceRatesEnabled) { - protectedCollectPolicyMenuItems.push({ + { translationKey: 'workspace.common.distanceRates', icon: Expensicons.Car, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.DISTANCE_RATES, - }); - } - - if (policy?.areWorkflowsEnabled) { - protectedCollectPolicyMenuItems.push({ + name: SCREENS.WORKSPACE.DISTANCE_RATES, + }, + { translationKey: 'workspace.common.workflows', icon: Expensicons.Workflows, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.WORKFLOWS, + name: SCREENS.WORKSPACE.WORKFLOWS, brickRoadIndicator: !isEmptyObject(policy?.errorFields?.reimburserEmail ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - }); - } - - if (policy?.areCategoriesEnabled) { - protectedCollectPolicyMenuItems.push({ + }, + { translationKey: 'workspace.common.categories', icon: Expensicons.Folder, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))), brickRoadIndicator: hasPolicyCategoryError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - routeName: SCREENS.WORKSPACE.CATEGORIES, - }); - } - - if (policy?.areTagsEnabled) { - protectedCollectPolicyMenuItems.push({ + name: SCREENS.WORKSPACE.CATEGORIES, + }, + { translationKey: 'workspace.common.tags', icon: Expensicons.Tag, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.TAGS, - }); - } - - if (policy?.tax?.trackingEnabled) { - protectedCollectPolicyMenuItems.push({ + name: SCREENS.WORKSPACE.TAGS, + }, + { translationKey: 'workspace.common.taxes', icon: Expensicons.Tax, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.TAXES, brickRoadIndicator: PolicyUtils.hasTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - }); - } - - protectedCollectPolicyMenuItems.push({ - translationKey: 'workspace.common.moreFeatures', - icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))), - routeName: SCREENS.WORKSPACE.MORE_FEATURES, - }); - - const menuItems: WorkspaceMenuItem[] = [ - { - translationKey: 'workspace.common.profile', - icon: Expensicons.Home, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))), - brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - routeName: SCREENS.WORKSPACE.PROFILE, + name: SCREENS.WORKSPACE.TAXES, }, { - translationKey: 'workspace.common.members', - icon: Expensicons.Users, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)))), - brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - routeName: SCREENS.WORKSPACE.MEMBERS, + translationKey: 'workspace.common.moreFeatures', + icon: Expensicons.Gear, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))), + name: SCREENS.WORKSPACE.MORE_FEATURES, }, - ...(isPaidGroupPolicy && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []), - ...(isFreeGroupPolicy && shouldShowProtectedItems ? protectedFreePolicyMenuItems : []), ]; const prevPolicy = usePrevious(policy); const route = useRoute>(); const enabledFeatureRouteName = route.params?.enabledFeatureRouteName ?? ''; - const enabledItem = protectedCollectPolicyMenuItems.find((item) => item.routeName === enabledFeatureRouteName); + const enabledItem = allMenuItems.find((item) => item.name === enabledFeatureRouteName); + const animatedHighlightStyle = useAnimatedHighlightStyle(!!enabledItem, 500, 150); + + useFocusEffect( + React.useCallback(() => { + if (!(isFreeGroupPolicy && shouldShowProtectedItems)) { + return; + } + + setMenuItems([ + SCREENS.WORKSPACE.PROFILE, + SCREENS.WORKSPACE.MEMBERS, + SCREENS.WORKSPACE.CARD, + SCREENS.WORKSPACE.REIMBURSE, + SCREENS.WORKSPACE.BILLS, + SCREENS.WORKSPACE.INVOICES, + SCREENS.WORKSPACE.TRAVEL, + ]); + }, [isFreeGroupPolicy, shouldShowProtectedItems, setMenuItems]), + ); + + useFocusEffect( + React.useCallback(() => { + if (!(isPaidGroupPolicy && shouldShowProtectedItems)) { + return; + } + + const screensToAdd: Array> = [SCREENS.WORKSPACE.PROFILE, SCREENS.WORKSPACE.MEMBERS, SCREENS.WORKSPACE.MORE_FEATURES]; + + if (policy?.areDistanceRatesEnabled) { + screensToAdd.push(SCREENS.WORKSPACE.DISTANCE_RATES); + } + + if (policy?.areWorkflowsEnabled) { + screensToAdd.push(SCREENS.WORKSPACE.WORKFLOWS); + } + + if (policy?.areCategoriesEnabled) { + screensToAdd.push(SCREENS.WORKSPACE.CATEGORIES); + } + + if (policy?.areTagsEnabled) { + screensToAdd.push(SCREENS.WORKSPACE.TAGS); + } + + if (policy?.tax?.trackingEnabled) { + screensToAdd.push(SCREENS.WORKSPACE.TAXES); + } + + setMenuItems(screensToAdd); + }, [ + isPaidGroupPolicy, + shouldShowProtectedItems, + setMenuItems, + policy?.areCategoriesEnabled, + policy?.areDistanceRatesEnabled, + policy?.areTagsEnabled, + policy?.tax?.trackingEnabled, + policy?.areWorkflowsEnabled, + ]), + ); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = @@ -281,22 +321,30 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r Ideally we should use MenuList component for MenuItems with singleExecution/Navigation actions. In this case where user can click on workspace avatar or menu items, we need to have a check for `isExecuting`. So, we are directly mapping menuItems. */} - {menuItems.map((item) => ( - - ))} + {allMenuItems + .filter((item) => item.name && menuItems.includes(item.name)) + .map((item) => ( + + + + ))} From 811debfb93b378b78276ba2195a4123b3322865d Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 28 Mar 2024 03:28:29 +0500 Subject: [PATCH 04/19] apply diff --- src/hooks/useAnimatedHighlightStyle.ts | 8 +- src/pages/workspace/WorkspaceInitialPage.tsx | 188 +++++++------------ 2 files changed, 77 insertions(+), 119 deletions(-) diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts index 4996e0bdb13e..b43515a1c07b 100644 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -1,7 +1,7 @@ import {useFocusEffect} from '@react-navigation/native'; import React from 'react'; import {InteractionManager} from 'react-native'; -import {interpolateColor, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; +import {interpolateColor, interpolate, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; import CONST from '@src/CONST'; import useTheme from './useTheme'; @@ -10,10 +10,13 @@ import useTheme from './useTheme'; */ export default function useAnimatedHighlightStyle(shouldHighlight: boolean, highlightDuration: number = CONST.ANIMATED_TRANSITION, delay = 100) { const highlightProgress = useSharedValue(0); + const heightProgress = useSharedValue(0); const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ backgroundColor: interpolateColor(highlightProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), + flex: interpolate(heightProgress.value, [0, 1], [0, 1]), + opacity: interpolate(heightProgress.value, [0, 1], [0, 1]), })); useFocusEffect( @@ -21,6 +24,7 @@ export default function useAnimatedHighlightStyle(shouldHighlight: boolean, high if (!shouldHighlight) { return; } + heightProgress.value = withTiming(1, {duration: highlightDuration}); InteractionManager.runAfterInteractions(() => { highlightProgress.value = withSequence( withDelay(delay, withTiming(0)), @@ -29,7 +33,7 @@ export default function useAnimatedHighlightStyle(shouldHighlight: boolean, high withTiming(0, {duration: highlightDuration}), ); }); - }, [shouldHighlight, highlightDuration, delay, highlightProgress]), + }, [shouldHighlight, highlightDuration, delay, highlightProgress, heightProgress]), ); return highlightBackgroundStyle; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index a3933e997bab..cd412b2a2134 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,11 +1,10 @@ -import {useFocusEffect, useNavigationState, useRoute} from '@react-navigation/native'; +import {useNavigationState, useRoute} from '@react-navigation/native'; import type {RouteProp} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import Animated, {FadeInUp} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; @@ -72,7 +71,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r const {singleExecution, isExecuting} = useSingleExecution(); const activeRoute = useNavigationState(getTopmostWorkspacesCentralPaneName); const {translate} = useLocalize(); - const [menuItems, setMenuItems] = useState>>([]); const policyID = policy?.id ?? ''; const policyName = policy?.name ?? ''; @@ -110,21 +108,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); const isFreeGroupPolicy = PolicyUtils.isFreeGroupPolicy(policy); - const allMenuItems: WorkspaceMenuItem[] = [ - { - translationKey: 'workspace.common.profile', - icon: Expensicons.Home, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))), - brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - name: SCREENS.WORKSPACE.PROFILE, - }, - { - translationKey: 'workspace.common.members', - icon: Expensicons.Users, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)))), - brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - name: SCREENS.WORKSPACE.MEMBERS, - }, + const protectedFreePolicyMenuItems: WorkspaceMenuItem[] = [ { translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, @@ -164,113 +148,91 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r : setIsCurrencyModalOpen(true), brickRoadIndicator: !isEmptyObject(reimbursementAccount?.errors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }, - { + ]; + + const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = []; + + if (policy?.areDistanceRatesEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.distanceRates', icon: Expensicons.Car, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)))), name: SCREENS.WORKSPACE.DISTANCE_RATES, - }, - { + }); + } + + if (policy?.areWorkflowsEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.workflows', icon: Expensicons.Workflows, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))), name: SCREENS.WORKSPACE.WORKFLOWS, brickRoadIndicator: !isEmptyObject(policy?.errorFields?.reimburserEmail ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - }, - { + }); + } + + if (policy?.areCategoriesEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.categories', icon: Expensicons.Folder, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))), brickRoadIndicator: hasPolicyCategoryError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, name: SCREENS.WORKSPACE.CATEGORIES, - }, - { + }); + } + + if (policy?.areTagsEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.tags', icon: Expensicons.Tag, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)))), name: SCREENS.WORKSPACE.TAGS, - }, - { + }); + } + + if (policy?.tax?.trackingEnabled) { + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.taxes', icon: Expensicons.Tax, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))), - brickRoadIndicator: PolicyUtils.hasTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, name: SCREENS.WORKSPACE.TAXES, + brickRoadIndicator: PolicyUtils.hasTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + }); + } + + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.moreFeatures', + icon: Expensicons.Gear, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))), + name: SCREENS.WORKSPACE.MORE_FEATURES, + }); + + const menuItems: WorkspaceMenuItem[] = [ + { + translationKey: 'workspace.common.profile', + icon: Expensicons.Home, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))), + brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + name: SCREENS.WORKSPACE.PROFILE, }, { - translationKey: 'workspace.common.moreFeatures', - icon: Expensicons.Gear, - action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))), - name: SCREENS.WORKSPACE.MORE_FEATURES, + translationKey: 'workspace.common.members', + icon: Expensicons.Users, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)))), + brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + name: SCREENS.WORKSPACE.MEMBERS, }, + ...(isPaidGroupPolicy && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []), + ...(isFreeGroupPolicy && shouldShowProtectedItems ? protectedFreePolicyMenuItems : []), ]; const prevPolicy = usePrevious(policy); const route = useRoute>(); const enabledFeatureRouteName = route.params?.enabledFeatureRouteName ?? ''; - const enabledItem = allMenuItems.find((item) => item.name === enabledFeatureRouteName); + const enabledItem = menuItems.find((item) => item.name === enabledFeatureRouteName); const animatedHighlightStyle = useAnimatedHighlightStyle(!!enabledItem, 500, 150); - useFocusEffect( - React.useCallback(() => { - if (!(isFreeGroupPolicy && shouldShowProtectedItems)) { - return; - } - - setMenuItems([ - SCREENS.WORKSPACE.PROFILE, - SCREENS.WORKSPACE.MEMBERS, - SCREENS.WORKSPACE.CARD, - SCREENS.WORKSPACE.REIMBURSE, - SCREENS.WORKSPACE.BILLS, - SCREENS.WORKSPACE.INVOICES, - SCREENS.WORKSPACE.TRAVEL, - ]); - }, [isFreeGroupPolicy, shouldShowProtectedItems, setMenuItems]), - ); - - useFocusEffect( - React.useCallback(() => { - if (!(isPaidGroupPolicy && shouldShowProtectedItems)) { - return; - } - - const screensToAdd: Array> = [SCREENS.WORKSPACE.PROFILE, SCREENS.WORKSPACE.MEMBERS, SCREENS.WORKSPACE.MORE_FEATURES]; - - if (policy?.areDistanceRatesEnabled) { - screensToAdd.push(SCREENS.WORKSPACE.DISTANCE_RATES); - } - - if (policy?.areWorkflowsEnabled) { - screensToAdd.push(SCREENS.WORKSPACE.WORKFLOWS); - } - - if (policy?.areCategoriesEnabled) { - screensToAdd.push(SCREENS.WORKSPACE.CATEGORIES); - } - - if (policy?.areTagsEnabled) { - screensToAdd.push(SCREENS.WORKSPACE.TAGS); - } - - if (policy?.tax?.trackingEnabled) { - screensToAdd.push(SCREENS.WORKSPACE.TAXES); - } - - setMenuItems(screensToAdd); - }, [ - isPaidGroupPolicy, - shouldShowProtectedItems, - setMenuItems, - policy?.areCategoriesEnabled, - policy?.areDistanceRatesEnabled, - policy?.areTagsEnabled, - policy?.tax?.trackingEnabled, - policy?.areWorkflowsEnabled, - ]), - ); - // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = isEmptyObject(policy) || @@ -321,30 +283,22 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r Ideally we should use MenuList component for MenuItems with singleExecution/Navigation actions. In this case where user can click on workspace avatar or menu items, we need to have a check for `isExecuting`. So, we are directly mapping menuItems. */} - {allMenuItems - .filter((item) => item.name && menuItems.includes(item.name)) - .map((item) => ( - - - - ))} + {menuItems.map((item) => ( + + ))} @@ -375,4 +329,4 @@ export default withPolicyAndFullscreenLoading( key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID ?? '0'}`, }, })(WorkspaceInitialPage), -); +); \ No newline at end of file From e4dec8f79ba4935656445210d4de99f813e63d0b Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 28 Mar 2024 03:48:39 +0500 Subject: [PATCH 05/19] prettier --- src/hooks/useAnimatedHighlightStyle.ts | 20 ++++++++++---------- src/pages/workspace/WorkspaceInitialPage.tsx | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts index b43515a1c07b..7de7717f2f2d 100644 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -1,22 +1,22 @@ import {useFocusEffect} from '@react-navigation/native'; import React from 'react'; import {InteractionManager} from 'react-native'; -import {interpolateColor, interpolate, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; +import {interpolate, interpolateColor, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; import CONST from '@src/CONST'; import useTheme from './useTheme'; /** - * Returns a highlight style that interpolates the colour giving a fading effect. + * Returns a highlight style that interpolates the colour, height and opacity giving a fading effect. */ export default function useAnimatedHighlightStyle(shouldHighlight: boolean, highlightDuration: number = CONST.ANIMATED_TRANSITION, delay = 100) { - const highlightProgress = useSharedValue(0); - const heightProgress = useSharedValue(0); + const repeatableProgress = useSharedValue(0); + const nonRepeatableProgress = useSharedValue(0); const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ - backgroundColor: interpolateColor(highlightProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), - flex: interpolate(heightProgress.value, [0, 1], [0, 1]), - opacity: interpolate(heightProgress.value, [0, 1], [0, 1]), + backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), + flex: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), + opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), })); useFocusEffect( @@ -24,16 +24,16 @@ export default function useAnimatedHighlightStyle(shouldHighlight: boolean, high if (!shouldHighlight) { return; } - heightProgress.value = withTiming(1, {duration: highlightDuration}); + nonRepeatableProgress.value = withTiming(1, {duration: highlightDuration}); InteractionManager.runAfterInteractions(() => { - highlightProgress.value = withSequence( + repeatableProgress.value = withSequence( withDelay(delay, withTiming(0)), withTiming(1, {duration: highlightDuration}), withDelay(delay, withTiming(1)), withTiming(0, {duration: highlightDuration}), ); }); - }, [shouldHighlight, highlightDuration, delay, highlightProgress, heightProgress]), + }, [shouldHighlight, highlightDuration, delay, repeatableProgress, nonRepeatableProgress]), ); return highlightBackgroundStyle; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index f4320b3ee4f6..c3c7f539f50c 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -293,7 +293,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r brickRoadIndicator={item.brickRoadIndicator} wrapperStyle={styles.sectionMenuItem} highlightStyle={enabledItem?.translationKey === item.translationKey ? [animatedHighlightStyle, {borderRadius: styles.border.borderRadius}] : undefined} - focused={!!(item.name && activeRoute?.startsWith(item.name))} + focused={!!(item.translationKey && activeRoute?.startsWith(item.name))} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu /> @@ -328,4 +328,4 @@ export default withPolicyAndFullscreenLoading( key: ({route}) => `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID ?? '0'}`, }, })(WorkspaceInitialPage), -); \ No newline at end of file +); From 23309a57d9975a3cb5674adc7ebe922854fbddb9 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 28 Mar 2024 04:42:25 +0500 Subject: [PATCH 06/19] prettier --- src/components/MenuItem.tsx | 10 +++-- src/hooks/useAnimatedHighlightStyle.ts | 42 +++++++++++--------- src/pages/workspace/WorkspaceInitialPage.tsx | 4 +- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 872cd08e0faf..66f525d17fe5 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -6,6 +6,7 @@ import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react import {View} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -149,8 +150,8 @@ type MenuItemBaseProps = { /** Whether item is focused or active */ focused?: boolean; - /** Style for the highlighted state */ - highlightStyle?: StyleProp>; + /** Whether item is highlighted */ + highlighted?: boolean; /** Should we disable this menu item? */ disabled?: boolean; @@ -289,7 +290,7 @@ function MenuItem( success = false, focused = false, disabled = false, - highlightStyle = {}, + highlighted = false, title, subtitle, shouldShowBasicTitle, @@ -329,6 +330,7 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); + const animatedHighlightStyle = useAnimatedHighlightStyle({shouldHighlight: highlighted, height: 56}); const [html, setHtml] = useState(''); const titleRef = useRef(''); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; @@ -432,7 +434,7 @@ function MenuItem( onPressIn={() => shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={onSecondaryInteraction} - wrapperStyle={highlightStyle} + wrapperStyle={animatedHighlightStyle} style={({pressed}) => [ containerStyle, diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts index 7de7717f2f2d..1712f7c54492 100644 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -1,40 +1,44 @@ -import {useFocusEffect} from '@react-navigation/native'; import React from 'react'; import {InteractionManager} from 'react-native'; import {interpolate, interpolateColor, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; import CONST from '@src/CONST'; import useTheme from './useTheme'; +type Props = { + height: number; + shouldHighlight: boolean; + highlightDuration?: number; + delay?: number; +}; + /** * Returns a highlight style that interpolates the colour, height and opacity giving a fading effect. */ -export default function useAnimatedHighlightStyle(shouldHighlight: boolean, highlightDuration: number = CONST.ANIMATED_TRANSITION, delay = 100) { +export default function useAnimatedHighlightStyle({shouldHighlight, highlightDuration = CONST.ANIMATED_TRANSITION, delay = CONST.ANIMATION_IN_TIMING, height}: Props) { const repeatableProgress = useSharedValue(0); const nonRepeatableProgress = useSharedValue(0); const theme = useTheme(); const highlightBackgroundStyle = useAnimatedStyle(() => ({ backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), - flex: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), + height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]), opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), })); - useFocusEffect( - React.useCallback(() => { - if (!shouldHighlight) { - return; - } + React.useEffect(() => { + if (!shouldHighlight) { + return; + } + InteractionManager.runAfterInteractions(() => { nonRepeatableProgress.value = withTiming(1, {duration: highlightDuration}); - InteractionManager.runAfterInteractions(() => { - repeatableProgress.value = withSequence( - withDelay(delay, withTiming(0)), - withTiming(1, {duration: highlightDuration}), - withDelay(delay, withTiming(1)), - withTiming(0, {duration: highlightDuration}), - ); - }); - }, [shouldHighlight, highlightDuration, delay, repeatableProgress, nonRepeatableProgress]), - ); + repeatableProgress.value = withSequence( + withDelay(delay, withTiming(0)), + withTiming(1, {duration: highlightDuration}), + withDelay(delay, withTiming(1)), + withTiming(0, {duration: highlightDuration}), + ); + }); + }, [shouldHighlight, highlightDuration, delay, repeatableProgress, nonRepeatableProgress]); - return highlightBackgroundStyle; + return shouldHighlight ? highlightBackgroundStyle : null; } diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index c3c7f539f50c..32a177bef1ae 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -14,7 +14,6 @@ import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; -import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useSingleExecution from '@hooks/useSingleExecution'; @@ -231,7 +230,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r const enabledFeatureRouteName = route.params?.enabledFeatureRouteName ?? ''; const enabledItem = menuItems.find((item) => item.name === enabledFeatureRouteName); - const animatedHighlightStyle = useAnimatedHighlightStyle(!!enabledItem, 500, 150); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = @@ -292,7 +290,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r onPress={item.action} brickRoadIndicator={item.brickRoadIndicator} wrapperStyle={styles.sectionMenuItem} - highlightStyle={enabledItem?.translationKey === item.translationKey ? [animatedHighlightStyle, {borderRadius: styles.border.borderRadius}] : undefined} + highlighted={enabledItem?.name === item.name} focused={!!(item.translationKey && activeRoute?.startsWith(item.name))} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu From 2c79caa945491a19fb8702d2d8b9239e0168e8f9 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 28 Mar 2024 04:48:01 +0500 Subject: [PATCH 07/19] type fix --- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 32a177bef1ae..d8522e8d500e 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -291,7 +291,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r brickRoadIndicator={item.brickRoadIndicator} wrapperStyle={styles.sectionMenuItem} highlighted={enabledItem?.name === item.name} - focused={!!(item.translationKey && activeRoute?.startsWith(item.name))} + focused={!!(item.translationKey && activeRoute?.startsWith(item.translationKey))} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu /> From 352b7d30fe4aa50bec87df2d982ec2a7ab6f41d7 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 28 Mar 2024 04:58:55 +0500 Subject: [PATCH 08/19] add easing --- src/hooks/useAnimatedHighlightStyle.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts index 1712f7c54492..735079ea0023 100644 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -1,6 +1,6 @@ import React from 'react'; import {InteractionManager} from 'react-native'; -import {interpolate, interpolateColor, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; +import {Easing, interpolate, interpolateColor, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; import CONST from '@src/CONST'; import useTheme from './useTheme'; @@ -30,12 +30,12 @@ export default function useAnimatedHighlightStyle({shouldHighlight, highlightDur return; } InteractionManager.runAfterInteractions(() => { - nonRepeatableProgress.value = withTiming(1, {duration: highlightDuration}); + nonRepeatableProgress.value = withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}); repeatableProgress.value = withSequence( withDelay(delay, withTiming(0)), - withTiming(1, {duration: highlightDuration}), + withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}), withDelay(delay, withTiming(1)), - withTiming(0, {duration: highlightDuration}), + withTiming(0, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}), ); }); }, [shouldHighlight, highlightDuration, delay, repeatableProgress, nonRepeatableProgress]); From 7b79d4470bfae521e21bdfb1528d1d1e361f36e3 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 3 Apr 2024 11:58:12 +0500 Subject: [PATCH 09/19] revert back to useEffect approach --- src/ROUTES.ts | 2 +- src/libs/Navigation/types.ts | 1 - src/libs/actions/Policy.ts | 14 ++++---- src/pages/workspace/WorkspaceInitialPage.tsx | 38 ++++++++++---------- 4 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 252afce9404e..8130c271a2db 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -452,7 +452,7 @@ const ROUTES = { WORKSPACE_NEW_ROOM: 'workspace/new-room', WORKSPACE_INITIAL: { route: 'settings/workspaces/:policyID', - getRoute: (policyID: string, featureRouteName?: string) => `settings/workspaces/${policyID}${featureRouteName ? `?enabledFeatureRouteName=${featureRouteName}` : ''}` as const, + getRoute: (policyID: string) => `settings/workspaces/${policyID}` as const, }, WORKSPACE_INVITE: { route: 'settings/workspaces/:policyID/invite', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 66c67ad9e8f9..b88c44b9aa70 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -630,7 +630,6 @@ type WorkspacesCentralPaneNavigatorParamList = { type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.INITIAL]: { policyID: string; - enabledFeatureRouteName?: string; }; [SCREENS.WORKSPACES_CENTRAL_PANE]: NavigatorScreenParams; }; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index a091b3bac3ae..30075135afa3 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3609,11 +3609,11 @@ function openPolicyDistanceRatesPage(policyID?: string) { API.read(READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE, params); } -function navigateWhenEnableFeature(policyID: string, featureRouteName: string, featureRoute: Route) { +function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { const isNarrowLayout = getIsNarrowLayout(); if (isNarrowLayout) { setTimeout(() => { - Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID, featureRouteName)); + Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID)); }, 1000); return; } @@ -3673,7 +3673,7 @@ function enablePolicyCategories(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.CATEGORIES, ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); } } @@ -3765,7 +3765,7 @@ function enablePolicyDistanceRates(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_DISTANCE_RATES, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.DISTANCE_RATES, ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)); + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)); } } @@ -3857,7 +3857,7 @@ function enablePolicyTags(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_TAGS, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.TAGS, ROUTES.WORKSPACE_TAGS.getRoute(policyID)); + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAGS.getRoute(policyID)); } } @@ -3970,7 +3970,7 @@ function enablePolicyTaxes(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_TAXES, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.TAXES, ROUTES.WORKSPACE_TAXES.getRoute(policyID)); + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_TAXES.getRoute(policyID)); } } @@ -4061,7 +4061,7 @@ function enablePolicyWorkflows(policyID: string, enabled: boolean) { API.write(WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, parameters, onyxData); if (enabled) { - navigateWhenEnableFeature(policyID, SCREENS.WORKSPACE.WORKFLOWS, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)); + navigateWhenEnableFeature(policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)); } } diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index d8522e8d500e..ed3298c9833f 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -43,7 +43,7 @@ type WorkspaceMenuItem = { icon: IconAsset; action: () => void; brickRoadIndicator?: ValueOf; - name?: ValueOf; + routeName?: ValueOf; }; type WorkspaceInitialPageOnyxProps = { @@ -112,31 +112,31 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r translationKey: 'workspace.common.card', icon: Expensicons.ExpensifyCard, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CARD.getRoute(policyID)))), - name: SCREENS.WORKSPACE.CARD, + routeName: SCREENS.WORKSPACE.CARD, }, { translationKey: 'workspace.common.reimburse', icon: Expensicons.Receipt, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REIMBURSE.getRoute(policyID)))), - name: SCREENS.WORKSPACE.REIMBURSE, + routeName: SCREENS.WORKSPACE.REIMBURSE, }, { translationKey: 'workspace.common.bills', icon: Expensicons.Bill, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_BILLS.getRoute(policyID)))), - name: SCREENS.WORKSPACE.BILLS, + routeName: SCREENS.WORKSPACE.BILLS, }, { translationKey: 'workspace.common.invoices', icon: Expensicons.Invoice, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)))), - name: SCREENS.WORKSPACE.INVOICES, + routeName: SCREENS.WORKSPACE.INVOICES, }, { translationKey: 'workspace.common.travel', icon: Expensicons.Luggage, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TRAVEL.getRoute(policyID)))), - name: SCREENS.WORKSPACE.TRAVEL, + routeName: SCREENS.WORKSPACE.TRAVEL, }, { translationKey: 'workspace.common.bankAccount', @@ -156,7 +156,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r translationKey: 'workspace.common.distanceRates', icon: Expensicons.Car, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)))), - name: SCREENS.WORKSPACE.DISTANCE_RATES, + routeName: SCREENS.WORKSPACE.DISTANCE_RATES, }); } @@ -165,7 +165,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r translationKey: 'workspace.common.workflows', icon: Expensicons.Workflows, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))), - name: SCREENS.WORKSPACE.WORKFLOWS, + routeName: SCREENS.WORKSPACE.WORKFLOWS, brickRoadIndicator: !isEmptyObject(policy?.errorFields?.reimburserEmail ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } @@ -176,7 +176,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r icon: Expensicons.Folder, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))), brickRoadIndicator: hasPolicyCategoryError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - name: SCREENS.WORKSPACE.CATEGORIES, + routeName: SCREENS.WORKSPACE.CATEGORIES, }); } @@ -185,7 +185,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r translationKey: 'workspace.common.tags', icon: Expensicons.Tag, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)))), - name: SCREENS.WORKSPACE.TAGS, + routeName: SCREENS.WORKSPACE.TAGS, }); } @@ -194,7 +194,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r translationKey: 'workspace.common.taxes', icon: Expensicons.Tax, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))), - name: SCREENS.WORKSPACE.TAXES, + routeName: SCREENS.WORKSPACE.TAXES, brickRoadIndicator: PolicyUtils.hasTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } @@ -203,7 +203,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r translationKey: 'workspace.common.moreFeatures', icon: Expensicons.Gear, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID)))), - name: SCREENS.WORKSPACE.MORE_FEATURES, + routeName: SCREENS.WORKSPACE.MORE_FEATURES, }); const menuItems: WorkspaceMenuItem[] = [ @@ -212,24 +212,22 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r icon: Expensicons.Home, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PROFILE.getRoute(policyID)))), brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - name: SCREENS.WORKSPACE.PROFILE, + routeName: SCREENS.WORKSPACE.PROFILE, }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)))), brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - name: SCREENS.WORKSPACE.MEMBERS, + routeName: SCREENS.WORKSPACE.MEMBERS, }, ...(isPaidGroupPolicy && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []), ...(isFreeGroupPolicy && shouldShowProtectedItems ? protectedFreePolicyMenuItems : []), ]; const prevPolicy = usePrevious(policy); - const route = useRoute>(); - const enabledFeatureRouteName = route.params?.enabledFeatureRouteName ?? ''; - - const enabledItem = menuItems.find((item) => item.name === enabledFeatureRouteName); + const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); + const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = @@ -290,8 +288,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r onPress={item.action} brickRoadIndicator={item.brickRoadIndicator} wrapperStyle={styles.sectionMenuItem} - highlighted={enabledItem?.name === item.name} - focused={!!(item.translationKey && activeRoute?.startsWith(item.translationKey))} + highlighted={enabledItem?.routeName === item.routeName} + focused={!!(item.routeName && activeRoute?.startsWith(item.routeName))} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu /> From 75f38d7c31aeb4da44a4919cb03b51f7fb3d71c9 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 3 Apr 2024 16:33:58 +0500 Subject: [PATCH 10/19] lint fix --- src/pages/workspace/WorkspaceInitialPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index ed3298c9833f..26b5af37a034 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,5 +1,4 @@ -import {useNavigationState, useRoute} from '@react-navigation/native'; -import type {RouteProp} from '@react-navigation/native'; +import {useNavigationState} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; From 128cfea4d7453af8bd62c326d9f1c4a65f2dd58a Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 3 Apr 2024 16:50:37 +0500 Subject: [PATCH 11/19] more lint fixes --- src/CONST.ts | 1 + src/hooks/useAnimatedHighlightStyle.ts | 2 +- src/libs/actions/Policy.ts | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index e7358b382f14..9ef988485974 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -63,6 +63,7 @@ const CONST = { // Note: Group and Self-DM excluded as these are not tied to a Workspace WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, + ANIMATED_HIGHLIGHT_DELAY: 500, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATION_IN_TIMING: 100, diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts index 735079ea0023..81f66c9bcdb4 100644 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -14,7 +14,7 @@ type Props = { /** * Returns a highlight style that interpolates the colour, height and opacity giving a fading effect. */ -export default function useAnimatedHighlightStyle({shouldHighlight, highlightDuration = CONST.ANIMATED_TRANSITION, delay = CONST.ANIMATION_IN_TIMING, height}: Props) { +export default function useAnimatedHighlightStyle({shouldHighlight, highlightDuration = CONST.ANIMATED_TRANSITION, delay = CONST.ANIMATED_HIGHLIGHT_DELAY, height}: Props) { const repeatableProgress = useSharedValue(0); const nonRepeatableProgress = useSharedValue(0); const theme = useTheme(); diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 30075135afa3..b1e37a880859 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -70,7 +70,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; import type { InvitedEmailsToAccountIDs, PersonalDetailsList, From 3ade71123ff13817071e08af15e8d06c957f0f8d Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 3 Apr 2024 16:57:05 +0500 Subject: [PATCH 12/19] menu item height prop --- src/CONST.ts | 1 + src/components/MenuItem.tsx | 6 +++++- src/hooks/useAnimatedHighlightStyle.ts | 2 +- src/pages/workspace/WorkspaceInitialPage.tsx | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 9ef988485974..49b52462fd34 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -64,6 +64,7 @@ const CONST = { WORKSPACE_ROOM_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL, chatTypes.POLICY_ROOM, chatTypes.POLICY_EXPENSE_CHAT], ANDROID_PACKAGE_NAME, ANIMATED_HIGHLIGHT_DELAY: 500, + ANIMATED_HIGHLIGHT_DURATION: 500, ANIMATED_TRANSITION: 300, ANIMATED_TRANSITION_FROM_VALUE: 100, ANIMATION_IN_TIMING: 100, diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 33bcf47f445e..85a9aa595974 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -250,6 +250,9 @@ type MenuItemBaseProps = { /** Adds padding to the left of the text when there is no icon. */ shouldPutLeftPaddingWhenNoIcon?: boolean; + + /** Expected height of the menu item. This is needed for the animated highlight. */ + height?: number; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -322,6 +325,7 @@ function MenuItem( contentFit = 'cover', isPaneMenu = false, shouldPutLeftPaddingWhenNoIcon = false, + height, }: MenuItemProps, ref: ForwardedRef, ) { @@ -330,7 +334,7 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); - const animatedHighlightStyle = useAnimatedHighlightStyle({shouldHighlight: highlighted, height: 56}); + const animatedHighlightStyle = useAnimatedHighlightStyle({shouldHighlight: highlighted, height: height ?? styles.sectionMenuItem.height}); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts index 81f66c9bcdb4..dc9ea6180602 100644 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -14,7 +14,7 @@ type Props = { /** * Returns a highlight style that interpolates the colour, height and opacity giving a fading effect. */ -export default function useAnimatedHighlightStyle({shouldHighlight, highlightDuration = CONST.ANIMATED_TRANSITION, delay = CONST.ANIMATED_HIGHLIGHT_DELAY, height}: Props) { +export default function useAnimatedHighlightStyle({shouldHighlight, highlightDuration = CONST.ANIMATED_HIGHLIGHT_DURATION, delay = CONST.ANIMATED_HIGHLIGHT_DELAY, height}: Props) { const repeatableProgress = useSharedValue(0); const nonRepeatableProgress = useSharedValue(0); const theme = useTheme(); diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 26b5af37a034..d0afc1469303 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -291,6 +291,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r focused={!!(item.routeName && activeRoute?.startsWith(item.routeName))} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu + height={styles.sectionMenuItem.height} /> ))} From 752fedfb7ec94d9f878a1cc8e104747a987662a6 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Wed, 3 Apr 2024 17:19:40 +0500 Subject: [PATCH 13/19] add missing doc comments --- src/hooks/useAnimatedHighlightStyle.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts index dc9ea6180602..691e96438068 100644 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ b/src/hooks/useAnimatedHighlightStyle.ts @@ -5,9 +5,13 @@ import CONST from '@src/CONST'; import useTheme from './useTheme'; type Props = { + /** Height of the item that is to be faded */ height: number; + /** Whether the item should be highlighted */ shouldHighlight: boolean; + /** Duration of the highlight animation */ highlightDuration?: number; + /** Delay before the highlight animation starts */ delay?: number; }; From a8ce2c72ec67562ab32d9289c915c834eed03a4f Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 03:51:27 +0500 Subject: [PATCH 14/19] fix animations on native --- src/components/MenuItem.tsx | 13 ++-- src/hooks/useAnimatedHighlightStyle.ts | 48 -------------- .../config.native.ts | 5 ++ src/hooks/useAnimatedHighlightStyle/config.ts | 7 ++ src/hooks/useAnimatedHighlightStyle/index.ts | 64 +++++++++++++++++++ src/pages/workspace/WorkspaceInitialPage.tsx | 1 - 6 files changed, 83 insertions(+), 55 deletions(-) delete mode 100644 src/hooks/useAnimatedHighlightStyle.ts create mode 100644 src/hooks/useAnimatedHighlightStyle/config.native.ts create mode 100644 src/hooks/useAnimatedHighlightStyle/config.ts create mode 100644 src/hooks/useAnimatedHighlightStyle/index.ts diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 85a9aa595974..4d3677aa4965 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -3,7 +3,7 @@ import type {ImageContentFit} from 'expo-image'; import type {ForwardedRef, ReactNode} from 'react'; import React, {forwardRef, useContext, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import {View} from 'react-native'; +import {StyleSheet, View} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; @@ -250,9 +250,6 @@ type MenuItemBaseProps = { /** Adds padding to the left of the text when there is no icon. */ shouldPutLeftPaddingWhenNoIcon?: boolean; - - /** Expected height of the menu item. This is needed for the animated highlight. */ - height?: number; }; type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps; @@ -325,7 +322,6 @@ function MenuItem( contentFit = 'cover', isPaneMenu = false, shouldPutLeftPaddingWhenNoIcon = false, - height, }: MenuItemProps, ref: ForwardedRef, ) { @@ -334,7 +330,12 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); - const animatedHighlightStyle = useAnimatedHighlightStyle({shouldHighlight: highlighted, height: height ?? styles.sectionMenuItem.height}); + const flattenedWrapperStyles = StyleSheet.flatten(wrapperStyle); + const animatedHighlightStyle = useAnimatedHighlightStyle({ + shouldHighlight: highlighted, + height: flattenedWrapperStyles?.height ? Number(flattenedWrapperStyles.height) : styles.sectionMenuItem.height, + borderRadius: flattenedWrapperStyles?.borderRadius ? Number(flattenedWrapperStyles.borderRadius) : styles.sectionMenuItem.borderRadius, + }); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; diff --git a/src/hooks/useAnimatedHighlightStyle.ts b/src/hooks/useAnimatedHighlightStyle.ts deleted file mode 100644 index 691e96438068..000000000000 --- a/src/hooks/useAnimatedHighlightStyle.ts +++ /dev/null @@ -1,48 +0,0 @@ -import React from 'react'; -import {InteractionManager} from 'react-native'; -import {Easing, interpolate, interpolateColor, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; -import CONST from '@src/CONST'; -import useTheme from './useTheme'; - -type Props = { - /** Height of the item that is to be faded */ - height: number; - /** Whether the item should be highlighted */ - shouldHighlight: boolean; - /** Duration of the highlight animation */ - highlightDuration?: number; - /** Delay before the highlight animation starts */ - delay?: number; -}; - -/** - * Returns a highlight style that interpolates the colour, height and opacity giving a fading effect. - */ -export default function useAnimatedHighlightStyle({shouldHighlight, highlightDuration = CONST.ANIMATED_HIGHLIGHT_DURATION, delay = CONST.ANIMATED_HIGHLIGHT_DELAY, height}: Props) { - const repeatableProgress = useSharedValue(0); - const nonRepeatableProgress = useSharedValue(0); - const theme = useTheme(); - - const highlightBackgroundStyle = useAnimatedStyle(() => ({ - backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), - height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]), - opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), - })); - - React.useEffect(() => { - if (!shouldHighlight) { - return; - } - InteractionManager.runAfterInteractions(() => { - nonRepeatableProgress.value = withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}); - repeatableProgress.value = withSequence( - withDelay(delay, withTiming(0)), - withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}), - withDelay(delay, withTiming(1)), - withTiming(0, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)}), - ); - }); - }, [shouldHighlight, highlightDuration, delay, repeatableProgress, nonRepeatableProgress]); - - return shouldHighlight ? highlightBackgroundStyle : null; -} diff --git a/src/hooks/useAnimatedHighlightStyle/config.native.ts b/src/hooks/useAnimatedHighlightStyle/config.native.ts new file mode 100644 index 000000000000..a62d3a33039e --- /dev/null +++ b/src/hooks/useAnimatedHighlightStyle/config.native.ts @@ -0,0 +1,5 @@ +const DELAY_FACTOR = 1.85; + +export default {}; + +export {DELAY_FACTOR}; diff --git a/src/hooks/useAnimatedHighlightStyle/config.ts b/src/hooks/useAnimatedHighlightStyle/config.ts new file mode 100644 index 000000000000..b8d8bc0dbe9e --- /dev/null +++ b/src/hooks/useAnimatedHighlightStyle/config.ts @@ -0,0 +1,7 @@ +import {isMobile} from '@libs/Browser'; + +const DELAY_FACTOR = isMobile() ? 1 : 0.5; + +export default {}; + +export {DELAY_FACTOR}; diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts new file mode 100644 index 000000000000..e438bd2473fa --- /dev/null +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -0,0 +1,64 @@ +import React from 'react'; +import {InteractionManager} from 'react-native'; +import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; +import useTheme from '@hooks/useTheme'; +import CONST from '@src/CONST'; +import {DELAY_FACTOR} from './config'; + +type Props = { + /** Border radius of the wrapper */ + borderRadius: number; + + /** Height of the item that is to be faded */ + height: number; + + /** Whether the item should be highlighted */ + shouldHighlight: boolean; + + /** Duration of the highlight animation */ + highlightDuration?: number; + + /** Delay before the highlight animation starts */ + delay?: number; +}; + +/** + * Returns a highlight style that interpolates the colour, height and opacity giving a fading effect. + */ +export default function useAnimatedHighlightStyle({ + borderRadius, + shouldHighlight, + highlightDuration = CONST.ANIMATED_HIGHLIGHT_DURATION, + delay = CONST.ANIMATED_HIGHLIGHT_DELAY, + height, +}: Props) { + const actualDelay = delay * DELAY_FACTOR; + const repeatableProgress = useSharedValue(0); + const nonRepeatableProgress = useSharedValue(shouldHighlight ? 0 : 1); + const theme = useTheme(); + + const highlightBackgroundStyle = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor(repeatableProgress.value, [0, 1], ['rgba(0, 0, 0, 0)', theme.border]), + height: interpolate(nonRepeatableProgress.value, [0, 1], [0, height]), + opacity: interpolate(nonRepeatableProgress.value, [0, 1], [0, 1]), + borderRadius, + })); + + React.useEffect(() => { + if (!shouldHighlight) { + return; + } + + InteractionManager.runAfterInteractions(() => { + runOnJS(() => { + nonRepeatableProgress.value = withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})); + repeatableProgress.value = withSequence( + withDelay(actualDelay, withTiming(1, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})), + withDelay(actualDelay, withTiming(0, {duration: highlightDuration, easing: Easing.inOut(Easing.ease)})), + ); + })(); + }); + }, [shouldHighlight, highlightDuration, actualDelay, repeatableProgress, nonRepeatableProgress]); + + return highlightBackgroundStyle; +} diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index d0afc1469303..26b5af37a034 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -291,7 +291,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r focused={!!(item.routeName && activeRoute?.startsWith(item.routeName))} hoverAndPressStyle={styles.hoveredComponentBG} isPaneMenu - height={styles.sectionMenuItem.height} /> ))} From 15c9f38cd35b09e72e5d76f641b3f99b5b6082f0 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 04:03:12 +0500 Subject: [PATCH 15/19] adjust the config for animation --- src/hooks/useAnimatedHighlightStyle/config.ts | 3 +-- src/hooks/useAnimatedHighlightStyle/index.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/hooks/useAnimatedHighlightStyle/config.ts b/src/hooks/useAnimatedHighlightStyle/config.ts index b8d8bc0dbe9e..aca34c8cab56 100644 --- a/src/hooks/useAnimatedHighlightStyle/config.ts +++ b/src/hooks/useAnimatedHighlightStyle/config.ts @@ -1,7 +1,6 @@ import {isMobile} from '@libs/Browser'; -const DELAY_FACTOR = isMobile() ? 1 : 0.5; - +const DELAY_FACTOR = isMobile() ? 1 : 0.2; export default {}; export {DELAY_FACTOR}; diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index e438bd2473fa..a1b4a7fc0fe2 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -1,8 +1,8 @@ import React from 'react'; import {InteractionManager} from 'react-native'; import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; -import useTheme from '@hooks/useTheme'; import CONST from '@src/CONST'; +import useTheme from '@hooks/useTheme'; import {DELAY_FACTOR} from './config'; type Props = { From 4d11b7fdb9a36d1695f908cadada03b533a188d3 Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 04:18:56 +0500 Subject: [PATCH 16/19] prettier --- src/hooks/useAnimatedHighlightStyle/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAnimatedHighlightStyle/index.ts b/src/hooks/useAnimatedHighlightStyle/index.ts index a1b4a7fc0fe2..e438bd2473fa 100644 --- a/src/hooks/useAnimatedHighlightStyle/index.ts +++ b/src/hooks/useAnimatedHighlightStyle/index.ts @@ -1,8 +1,8 @@ import React from 'react'; import {InteractionManager} from 'react-native'; import {Easing, interpolate, interpolateColor, runOnJS, useAnimatedStyle, useSharedValue, withDelay, withSequence, withTiming} from 'react-native-reanimated'; -import CONST from '@src/CONST'; import useTheme from '@hooks/useTheme'; +import CONST from '@src/CONST'; import {DELAY_FACTOR} from './config'; type Props = { From 942d665b243da7106513fcec68861a22a34a40af Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Thu, 4 Apr 2024 16:57:48 +0500 Subject: [PATCH 17/19] fix regression --- src/components/HighlightableMenuItem.tsx | 36 ++++++++++++++++++++ src/components/MenuItem.tsx | 20 ++++------- src/pages/workspace/WorkspaceInitialPage.tsx | 4 +-- 3 files changed, 45 insertions(+), 15 deletions(-) create mode 100644 src/components/HighlightableMenuItem.tsx diff --git a/src/components/HighlightableMenuItem.tsx b/src/components/HighlightableMenuItem.tsx new file mode 100644 index 000000000000..81d2cdaa4646 --- /dev/null +++ b/src/components/HighlightableMenuItem.tsx @@ -0,0 +1,36 @@ +import type {ForwardedRef} from 'react'; +import {forwardRef} from 'react'; +import type {View} from 'react-native'; +import {StyleSheet} from 'react-native'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; +import useThemeStyles from '@hooks/useThemeStyles'; +import MenuItem from './MenuItem'; +import type {MenuItemProps} from './MenuItem'; + +type Props = MenuItemProps & { + highlighted?: boolean; +}; + +function HighlightableMenuItem({wrapperStyle, highlighted, ...restOfProps}: Props, ref: ForwardedRef) { + const styles = useThemeStyles(); + + const flattenedWrapperStyles = StyleSheet.flatten(wrapperStyle); + const animatedHighlightStyle = useAnimatedHighlightStyle({ + shouldHighlight: highlighted ?? false, + height: flattenedWrapperStyles?.height ? Number(flattenedWrapperStyles.height) : styles.sectionMenuItem.height, + borderRadius: flattenedWrapperStyles?.borderRadius ? Number(flattenedWrapperStyles.borderRadius) : styles.sectionMenuItem.borderRadius, + }); + + return ( + // eslint-disable-next-line react/jsx-props-no-spreading + + ); +} + +HighlightableMenuItem.displayName = 'HighlightableMenuItem'; + +export default forwardRef(HighlightableMenuItem); diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 4d3677aa4965..4d6f79bd0196 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -3,10 +3,9 @@ import type {ImageContentFit} from 'expo-image'; import type {ForwardedRef, ReactNode} from 'react'; import React, {forwardRef, useContext, useMemo} from 'react'; import type {GestureResponderEvent, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import {StyleSheet, View} from 'react-native'; +import {View} from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; -import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -72,6 +71,9 @@ type MenuItemBaseProps = { /** Used to apply offline styles to child text components */ style?: StyleProp; + /** Outer wrapper styles */ + outerWrapperStyle?: StyleProp; + /** Any additional styles to apply */ wrapperStyle?: StyleProp; @@ -150,9 +152,6 @@ type MenuItemBaseProps = { /** Whether item is focused or active */ focused?: boolean; - /** Whether item is highlighted */ - highlighted?: boolean; - /** Should we disable this menu item? */ disabled?: boolean; @@ -261,6 +260,7 @@ function MenuItem( badgeText, style, wrapperStyle, + outerWrapperStyle, containerStyle, titleStyle, hoverAndPressStyle, @@ -290,7 +290,6 @@ function MenuItem( success = false, focused = false, disabled = false, - highlighted = false, title, subtitle, shouldShowBasicTitle, @@ -330,13 +329,8 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); - const flattenedWrapperStyles = StyleSheet.flatten(wrapperStyle); - const animatedHighlightStyle = useAnimatedHighlightStyle({ - shouldHighlight: highlighted, - height: flattenedWrapperStyles?.height ? Number(flattenedWrapperStyles.height) : styles.sectionMenuItem.height, - borderRadius: flattenedWrapperStyles?.borderRadius ? Number(flattenedWrapperStyles.borderRadius) : styles.sectionMenuItem.borderRadius, - }); const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; + const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; const fallbackAvatarSize = viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT; @@ -436,7 +430,7 @@ function MenuItem( onPressIn={() => shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={onSecondaryInteraction} - wrapperStyle={animatedHighlightStyle} + wrapperStyle={outerWrapperStyle} style={({pressed}) => [ containerStyle, diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 68c5f48e2ff1..6d67b4549f29 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -8,8 +8,8 @@ import type {ValueOf} from 'type-fest'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import HighlightableMenuItem from '@components/HighlightableMenuItem'; import * as Expensicons from '@components/Icon/Expensicons'; -import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -278,7 +278,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r In this case where user can click on workspace avatar or menu items, we need to have a check for `isExecuting`. So, we are directly mapping menuItems. */} {menuItems.map((item) => ( - Date: Thu, 4 Apr 2024 17:03:54 +0500 Subject: [PATCH 18/19] lint --- src/components/HighlightableMenuItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/HighlightableMenuItem.tsx b/src/components/HighlightableMenuItem.tsx index 81d2cdaa4646..5cf401fdc8be 100644 --- a/src/components/HighlightableMenuItem.tsx +++ b/src/components/HighlightableMenuItem.tsx @@ -22,8 +22,8 @@ function HighlightableMenuItem({wrapperStyle, highlighted, ...restOfProps}: Prop }); return ( - // eslint-disable-next-line react/jsx-props-no-spreading Date: Thu, 4 Apr 2024 18:29:37 +0500 Subject: [PATCH 19/19] handle comments --- src/components/HighlightableMenuItem.tsx | 5 +++-- src/hooks/useAnimatedHighlightStyle/config.ts | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/HighlightableMenuItem.tsx b/src/components/HighlightableMenuItem.tsx index 5cf401fdc8be..7b8a431003ee 100644 --- a/src/components/HighlightableMenuItem.tsx +++ b/src/components/HighlightableMenuItem.tsx @@ -1,5 +1,5 @@ import type {ForwardedRef} from 'react'; -import {forwardRef} from 'react'; +import React, {forwardRef} from 'react'; import type {View} from 'react-native'; import {StyleSheet} from 'react-native'; import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; @@ -8,6 +8,7 @@ import MenuItem from './MenuItem'; import type {MenuItemProps} from './MenuItem'; type Props = MenuItemProps & { + /** Should the menu item be highlighted? */ highlighted?: boolean; }; @@ -25,7 +26,7 @@ function HighlightableMenuItem({wrapperStyle, highlighted, ...restOfProps}: Prop ); diff --git a/src/hooks/useAnimatedHighlightStyle/config.ts b/src/hooks/useAnimatedHighlightStyle/config.ts index aca34c8cab56..6010c8c33aa7 100644 --- a/src/hooks/useAnimatedHighlightStyle/config.ts +++ b/src/hooks/useAnimatedHighlightStyle/config.ts @@ -1,5 +1,7 @@ import {isMobile} from '@libs/Browser'; +// It takes varying amount of time to navigate to a new page on mobile and desktop +// This variable takes that into account const DELAY_FACTOR = isMobile() ? 1 : 0.2; export default {};