diff --git a/src/CONST.ts b/src/CONST.ts index 4fcc1cada6ff8..ed094f65ccc4e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6413,6 +6413,10 @@ const CONST = { RENAME_SAVED_SEARCH: 'renameSavedSearch', QUICK_ACTION_BUTTON: 'quickActionButton', WORKSAPCE_CHAT_CREATE: 'workspaceChatCreate', + SEARCH_FILTER_BUTTON_TOOLTIP: 'filterButtonTooltip', + BOTTOM_NAV_INBOX_TOOLTIP: 'bottomNavInboxTooltip', + LHN_WORKSPACE_CHAT_TOOLTIP: 'workspaceChatLHNTooltip', + GLOBAL_CREATE_TOOLTIP: 'globalCreateTooltip', }, } as const; diff --git a/src/components/FloatingActionButton.tsx b/src/components/FloatingActionButton.tsx index 3c831301db8bc..e0f0ff4e6dcdc 100644 --- a/src/components/FloatingActionButton.tsx +++ b/src/components/FloatingActionButton.tsx @@ -5,10 +5,16 @@ import type {GestureResponderEvent, Role, Text, View} from 'react-native'; import {Platform} from 'react-native'; import Animated, {createAnimatedPropAdapter, Easing, interpolateColor, processColor, useAnimatedProps, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import Svg, {Path} from 'react-native-svg'; +import useBottomTabIsFocused from '@hooks/useBottomTabIsFocused'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import {PressableWithoutFeedback} from './Pressable'; +import {useProductTrainingContext} from './ProductTrainingContext'; +import EducationalTooltip from './Tooltip/EducationalTooltip'; const AnimatedPath = Animated.createAnimatedComponent(Path); AnimatedPath.displayName = 'AnimatedPath'; @@ -56,6 +62,14 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo const styles = useThemeStyles(); const borderRadius = styles.floatingActionButton.borderRadius; const fabPressable = useRef(null); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const platform = getPlatform(); + const isNarrowScreenOnWeb = shouldUseNarrowLayout && platform === CONST.PLATFORM.WEB; + const isFocused = useBottomTabIsFocused(); + const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( + CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.GLOBAL_CREATE_TOOLTIP, + isFocused, + ); const sharedValue = useSharedValue(isActive ? 1 : 0); const buttonRef = ref; @@ -97,32 +111,45 @@ function FloatingActionButton({onPress, isActive, accessibilityLabel, role}: Flo }; return ( - { - fabPressable.current = el ?? null; - if (buttonRef && 'current' in buttonRef) { - buttonRef.current = el ?? null; - } + {}} - role={role} - shouldUseHapticsOnLongPress={false} + shouldUseOverlay + shiftHorizontal={isNarrowScreenOnWeb ? 0 : variables.fabTooltipShiftHorizontal} + renderTooltipContent={renderProductTrainingTooltip} + wrapperStyle={styles.productTrainingTooltipWrapper} + onHideTooltip={hideProductTrainingTooltip} > - - - - - - + { + fabPressable.current = el ?? null; + if (buttonRef && 'current' in buttonRef) { + buttonRef.current = el ?? null; + } + }} + style={[styles.h100, styles.bottomTabBarItem]} + accessibilityLabel={accessibilityLabel} + onPress={toggleFabAction} + onLongPress={() => {}} + role={role} + shouldUseHapticsOnLongPress={false} + > + + + + + + + ); } diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 6b8cf173b0fd6..efdd9659c8459 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import React, {useCallback, useRef, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, ViewStyle} from 'react-native'; import {StyleSheet, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -47,19 +47,21 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); - + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); + const isActiveWorkspaceChat = ReportUtils.isPolicyExpenseChat(report) && report?.isOwnPolicyExpenseChat && activePolicyID === report?.policyID; const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const session = useSession(); - - // Guides are assigned for the MANAGE_TEAM onboarding action, except for emails that have a '+'. const isOnboardingGuideAssigned = introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+'); - const shouldShowToooltipOnThisReport = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); + const shouldShowGetStartedTooltip = isOnboardingGuideAssigned ? ReportUtils.isAdminRoom(report) : ReportUtils.isConciergeChatReport(report); - const shouldShowGetStartedTooltip = shouldShowToooltipOnThisReport && isScreenFocused; - const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( - CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR, - shouldShowGetStartedTooltip, - ); + const {tooltipToRender, shouldShowTooltip} = useMemo(() => { + const tooltip = shouldShowGetStartedTooltip ? CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.CONCEIRGE_LHN_GBR : CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.LHN_WORKSPACE_CHAT_TOOLTIP; + + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return {tooltipToRender: tooltip, shouldShowTooltip: shouldUseNarrowLayout ? isScreenFocused : true}; + }, [shouldShowGetStartedTooltip, isScreenFocused, shouldUseNarrowLayout]); + + const {shouldShowProductTrainingTooltip, renderProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext(tooltipToRender, shouldShowTooltip); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); @@ -156,17 +158,18 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti needsOffscreenAlphaCompositing > diff --git a/src/components/ProductTrainingContext/PRODUCT_TRAINING_TOOLTIP_DATA.ts b/src/components/ProductTrainingContext/PRODUCT_TRAINING_TOOLTIP_DATA.ts deleted file mode 100644 index d7f2a27d94d2f..0000000000000 --- a/src/components/ProductTrainingContext/PRODUCT_TRAINING_TOOLTIP_DATA.ts +++ /dev/null @@ -1,67 +0,0 @@ -import type {ValueOf} from 'type-fest'; -import {dismissProductTraining} from '@libs/actions/Welcome'; -import CONST from '@src/CONST'; -import type {TranslationPaths} from '@src/languages/types'; - -const {CONCEIRGE_LHN_GBR, RENAME_SAVED_SEARCH, WORKSAPCE_CHAT_CREATE, QUICK_ACTION_BUTTON} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; - -type ProductTrainingTooltipName = ValueOf; - -type ShouldShowConditionProps = { - shouldUseNarrowLayout?: boolean; -}; - -type TooltipData = { - content: Array<{text: TranslationPaths; isBold: boolean}>; - onHideTooltip: () => void; - name: ProductTrainingTooltipName; - priority: number; - shouldShow: (props: ShouldShowConditionProps) => boolean; -}; - -const PRODUCT_TRAINING_TOOLTIP_DATA: Record = { - [CONCEIRGE_LHN_GBR]: { - content: [ - {text: 'productTrainingTooltip.conciergeLHNGBR.part1', isBold: false}, - {text: 'productTrainingTooltip.conciergeLHNGBR.part2', isBold: true}, - ], - onHideTooltip: () => dismissProductTraining(CONCEIRGE_LHN_GBR), - name: CONCEIRGE_LHN_GBR, - priority: 1300, - shouldShow: ({shouldUseNarrowLayout}) => !!shouldUseNarrowLayout, - }, - [RENAME_SAVED_SEARCH]: { - content: [ - {text: 'productTrainingTooltip.saveSearchTooltip.part1', isBold: true}, - {text: 'productTrainingTooltip.saveSearchTooltip.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(RENAME_SAVED_SEARCH), - name: RENAME_SAVED_SEARCH, - priority: 1250, - shouldShow: ({shouldUseNarrowLayout}) => !shouldUseNarrowLayout, - }, - [QUICK_ACTION_BUTTON]: { - content: [ - {text: 'productTrainingTooltip.quickActionButton.part1', isBold: true}, - {text: 'productTrainingTooltip.quickActionButton.part2', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(QUICK_ACTION_BUTTON), - name: QUICK_ACTION_BUTTON, - priority: 1200, - shouldShow: () => true, - }, - [WORKSAPCE_CHAT_CREATE]: { - content: [ - {text: 'productTrainingTooltip.workspaceChatCreate.part1', isBold: false}, - {text: 'productTrainingTooltip.workspaceChatCreate.part2', isBold: true}, - {text: 'productTrainingTooltip.workspaceChatCreate.part3', isBold: false}, - ], - onHideTooltip: () => dismissProductTraining(WORKSAPCE_CHAT_CREATE), - name: WORKSAPCE_CHAT_CREATE, - priority: 1100, - shouldShow: () => true, - }, -}; - -export default PRODUCT_TRAINING_TOOLTIP_DATA; -export type {ProductTrainingTooltipName}; diff --git a/src/components/ProductTrainingContext/TOOLTIPS.ts b/src/components/ProductTrainingContext/TOOLTIPS.ts new file mode 100644 index 0000000000000..dc2a761a4903e --- /dev/null +++ b/src/components/ProductTrainingContext/TOOLTIPS.ts @@ -0,0 +1,118 @@ +import type {ValueOf} from 'type-fest'; +import {dismissProductTraining} from '@libs/actions/Welcome'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; + +const { + CONCEIRGE_LHN_GBR, + RENAME_SAVED_SEARCH, + WORKSAPCE_CHAT_CREATE, + QUICK_ACTION_BUTTON, + SEARCH_FILTER_BUTTON_TOOLTIP, + BOTTOM_NAV_INBOX_TOOLTIP, + LHN_WORKSPACE_CHAT_TOOLTIP, + GLOBAL_CREATE_TOOLTIP, +} = CONST.PRODUCT_TRAINING_TOOLTIP_NAMES; + +type ProductTrainingTooltipName = ValueOf; + +type ShouldShowConditionProps = { + shouldUseNarrowLayout?: boolean; +}; + +type TooltipData = { + content: Array<{text: TranslationPaths; isBold: boolean}>; + onHideTooltip: () => void; + name: ProductTrainingTooltipName; + priority: number; + shouldShow: (props: ShouldShowConditionProps) => boolean; +}; + +const TOOLTIPS: Record = { + [CONCEIRGE_LHN_GBR]: { + content: [ + {text: 'productTrainingTooltip.conciergeLHNGBR.part1', isBold: false}, + {text: 'productTrainingTooltip.conciergeLHNGBR.part2', isBold: true}, + ], + onHideTooltip: () => dismissProductTraining(CONCEIRGE_LHN_GBR), + name: CONCEIRGE_LHN_GBR, + priority: 1300, + shouldShow: ({shouldUseNarrowLayout}) => !!shouldUseNarrowLayout, + }, + [RENAME_SAVED_SEARCH]: { + content: [ + {text: 'productTrainingTooltip.saveSearchTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.saveSearchTooltip.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(RENAME_SAVED_SEARCH), + name: RENAME_SAVED_SEARCH, + priority: 1250, + shouldShow: ({shouldUseNarrowLayout}) => !shouldUseNarrowLayout, + }, + [GLOBAL_CREATE_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.globalCreateTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.globalCreateTooltip.part2', isBold: false}, + {text: 'productTrainingTooltip.globalCreateTooltip.part3', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(GLOBAL_CREATE_TOOLTIP), + name: GLOBAL_CREATE_TOOLTIP, + priority: 1200, + shouldShow: () => true, + }, + [QUICK_ACTION_BUTTON]: { + content: [ + {text: 'productTrainingTooltip.quickActionButton.part1', isBold: true}, + {text: 'productTrainingTooltip.quickActionButton.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(QUICK_ACTION_BUTTON), + name: QUICK_ACTION_BUTTON, + priority: 1150, + shouldShow: () => true, + }, + [WORKSAPCE_CHAT_CREATE]: { + content: [ + {text: 'productTrainingTooltip.workspaceChatCreate.part1', isBold: true}, + {text: 'productTrainingTooltip.workspaceChatCreate.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(WORKSAPCE_CHAT_CREATE), + name: WORKSAPCE_CHAT_CREATE, + priority: 1100, + shouldShow: () => true, + }, + [SEARCH_FILTER_BUTTON_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.searchFilterButtonTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.searchFilterButtonTooltip.part2', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(SEARCH_FILTER_BUTTON_TOOLTIP), + name: SEARCH_FILTER_BUTTON_TOOLTIP, + priority: 1000, + shouldShow: () => true, + }, + [BOTTOM_NAV_INBOX_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.bottomNavInboxTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.bottomNavInboxTooltip.part2', isBold: false}, + {text: 'productTrainingTooltip.bottomNavInboxTooltip.part3', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(BOTTOM_NAV_INBOX_TOOLTIP), + name: BOTTOM_NAV_INBOX_TOOLTIP, + priority: 900, + shouldShow: () => true, + }, + [LHN_WORKSPACE_CHAT_TOOLTIP]: { + content: [ + {text: 'productTrainingTooltip.workspaceChatTooltip.part1', isBold: true}, + {text: 'productTrainingTooltip.workspaceChatTooltip.part2', isBold: false}, + {text: 'productTrainingTooltip.workspaceChatTooltip.part3', isBold: false}, + ], + onHideTooltip: () => dismissProductTraining(LHN_WORKSPACE_CHAT_TOOLTIP), + name: LHN_WORKSPACE_CHAT_TOOLTIP, + priority: 800, + shouldShow: () => true, + }, +}; + +export default TOOLTIPS; +export type {ProductTrainingTooltipName}; diff --git a/src/components/ProductTrainingContext/index.tsx b/src/components/ProductTrainingContext/index.tsx index 92997fe70af3c..7cfcf4d3bfa70 100644 --- a/src/components/ProductTrainingContext/index.tsx +++ b/src/components/ProductTrainingContext/index.tsx @@ -9,11 +9,12 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {hasCompletedGuidedSetupFlowSelector} from '@libs/onboardingSelectors'; -import Permissions from '@libs/Permissions'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import type {ProductTrainingTooltipName} from './PRODUCT_TRAINING_TOOLTIP_DATA'; -import PRODUCT_TRAINING_TOOLTIP_DATA from './PRODUCT_TRAINING_TOOLTIP_DATA'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import type {ProductTrainingTooltipName} from './TOOLTIPS'; +import TOOLTIPS from './TOOLTIPS'; type ProductTrainingContextType = { shouldRenderTooltip: (tooltipName: ProductTrainingTooltipName) => boolean; @@ -30,11 +31,10 @@ const ProductTrainingContext = createContext({ function ProductTrainingContextProvider({children}: ChildrenProps) { const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT); const hasBeenAddedToNudgeMigration = !!tryNewDot?.nudgeMigration?.timestamp; - const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { + const [isOnboardingCompleted = true, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { selector: hasCompletedGuidedSetupFlowSelector, }); const [dismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); - const [allBetas] = useOnyx(ONYXKEYS.BETAS); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [activeTooltips, setActiveTooltips] = useState>(new Set()); @@ -58,7 +58,7 @@ function ProductTrainingContextProvider({children}: ChildrenProps) { const sortedTooltips = Array.from(activeTooltips) .map((name) => ({ name, - priority: PRODUCT_TRAINING_TOOLTIP_DATA[name]?.priority ?? 0, + priority: TOOLTIPS[name]?.priority ?? 0, })) .sort((a, b) => b.priority - a.priority); @@ -73,14 +73,22 @@ function ProductTrainingContextProvider({children}: ChildrenProps) { const shouldTooltipBeVisible = useCallback( (tooltipName: ProductTrainingTooltipName) => { + if (isLoadingOnyxValue(isOnboardingCompletedMetadata)) { + return false; + } + const isDismissed = !!dismissedProductTraining?.[tooltipName]; - if (isDismissed || !Permissions.shouldShowProductTrainingElements(allBetas)) { + if (isDismissed) { return false; } - const tooltipConfig = PRODUCT_TRAINING_TOOLTIP_DATA[tooltipName]; + const tooltipConfig = TOOLTIPS[tooltipName]; - if (!isOnboardingCompleted && !hasBeenAddedToNudgeMigration) { + // if hasBeenAddedToNudgeMigration is true, and welcome modal is not dismissed, don't show tooltip + if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.[CONST.MIGRATED_USER_WELCOME_MODAL]) { + return false; + } + if (isOnboardingCompleted === false) { return false; } @@ -88,7 +96,7 @@ function ProductTrainingContextProvider({children}: ChildrenProps) { shouldUseNarrowLayout, }); }, - [allBetas, dismissedProductTraining, hasBeenAddedToNudgeMigration, isOnboardingCompleted, shouldUseNarrowLayout], + [dismissedProductTraining, hasBeenAddedToNudgeMigration, isOnboardingCompleted, isOnboardingCompletedMetadata, shouldUseNarrowLayout], ); const registerTooltip = useCallback( @@ -156,21 +164,21 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou }, [tooltipName, registerTooltip, unregisterTooltip, shouldShow]); const renderProductTrainingTooltip = useCallback(() => { - const tooltip = PRODUCT_TRAINING_TOOLTIP_DATA[tooltipName]; + const tooltip = TOOLTIPS[tooltipName]; return ( - + - + {tooltip.content.map(({text, isBold}) => { const translatedText = translate(text); return ( {translatedText} @@ -183,12 +191,14 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou styles.alignItemsCenter, styles.flexRow, styles.flexWrap, - styles.gap1, + styles.gap3, styles.justifyContentCenter, + styles.mw100, styles.p2, - styles.quickActionTooltipSubtitle, + styles.productTrainingTooltipText, styles.textAlignCenter, styles.textBold, + styles.textWrap, theme.tooltipHighlightText, tooltipName, translate, @@ -199,7 +209,7 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou }, [shouldRenderTooltip, tooltipName]); const hideProductTrainingTooltip = useCallback(() => { - const tooltip = PRODUCT_TRAINING_TOOLTIP_DATA[tooltipName]; + const tooltip = TOOLTIPS[tooltipName]; tooltip.onHideTooltip(); unregisterTooltip(tooltipName); }, [tooltipName, unregisterTooltip]); @@ -207,7 +217,7 @@ const useProductTrainingContext = (tooltipName: ProductTrainingTooltipName, shou return { renderProductTrainingTooltip, hideProductTrainingTooltip, - shouldShowProductTrainingTooltip: shouldShow && shouldShowProductTrainingTooltip, + shouldShowProductTrainingTooltip, }; }; diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index a78845f126d28..21a5832052c08 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -1,3 +1,4 @@ +import {useIsFocused} from '@react-navigation/native'; import React, {useMemo, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -8,6 +9,8 @@ import ConfirmModal from '@components/ConfirmModal'; import DecisionModal from '@components/DecisionModal'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; +import {useProductTrainingContext} from '@components/ProductTrainingContext'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -55,6 +58,11 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); + const isFocused = useIsFocused(); + const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( + CONST.PRODUCT_TRAINING_TOOLTIP_NAMES.SEARCH_FILTER_BUTTON_TOOLTIP, + isFocused, + ); const {status, hash} = queryJSON; @@ -348,12 +356,25 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { shouldUseStyleUtilityForAnchorPosition /> ) : ( -