diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 707c66b89c8c6..af6f092c07c2d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -215,6 +215,9 @@ const ONYXKEYS = { /** The NVP containing all information related to educational tooltip in workspace chat */ NVP_WORKSPACE_TOOLTIP: 'workspaceTooltip', + /** Whether to hide gbr tooltip */ + NVP_SHOULD_HIDE_GBR_TOOLTIP: 'nvp_should_hide_gbr_tooltip', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -949,6 +952,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_PRIVATE_AMOUNT_OWED]: number; [ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END]: number; [ONYXKEYS.NVP_WORKSPACE_TOOLTIP]: OnyxTypes.WorkspaceTooltip; + [ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP]: boolean; [ONYXKEYS.NVP_PRIVATE_CANCELLATION_DETAILS]: OnyxTypes.CancellationDetails[]; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 568839d6c9ae7..624e8f18e69e1 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -24,6 +24,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails} from '@src/types/onyx'; import OptionRowLHNData from './OptionRowLHNData'; +import OptionRowRendererComponent from './OptionRowRendererComponent'; import type {LHNOptionsListProps, RenderItemProps} from './types'; const keyExtractor = (item: string) => `report_${item}`; @@ -267,6 +268,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio ref={flashListRef} indicatorStyle="white" keyboardShouldPersistTaps="always" + CellRendererComponent={OptionRowRendererComponent} contentContainerStyle={StyleSheet.flatten(contentContainerStyles)} data={data} testID="lhn-options-list" diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 8ec96dbf24853..e1769db04cbec 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -14,6 +14,7 @@ import PressableWithSecondaryInteraction from '@components/PressableWithSecondar import SubscriptAvatar from '@components/SubscriptAvatar'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; +import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -21,6 +22,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import DomUtils from '@libs/DomUtils'; +import hasCompletedGuidedSetupFlowSelector from '@libs/hasCompletedGuidedSetupFlowSelector'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; import Performance from '@libs/Performance'; @@ -28,6 +30,8 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import * as ReportUtils from '@libs/ReportUtils'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import variables from '@styles/variables'; +import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -38,24 +42,53 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const styles = useThemeStyles(); const popoverAnchor = useRef(null); const StyleUtils = useStyleUtils(); - const isFocusedRef = useRef(true); + const [isScreenFocused, setIsScreenFocused] = useState(false); const {shouldUseNarrowLayout} = useResponsiveLayout(); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${optionItem?.reportID || -1}`); + const [isFirstTimeNewExpensifyUser] = useOnyx(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER); + const [hasCompletedGuidedSetupFlow] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { + selector: hasCompletedGuidedSetupFlowSelector, + }); + const [shouldHideGBRTooltip] = useOnyx(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP); const {translate} = useLocalize(); const [isContextMenuActive, setIsContextMenuActive] = useState(false); useFocusEffect( useCallback(() => { - isFocusedRef.current = true; + setIsScreenFocused(true); return () => { - isFocusedRef.current = false; + setIsScreenFocused(false); }; }, []), ); + const renderGBRTooltip = useCallback( + () => ( + + + {translate('sidebarScreen.tooltip')} + + ), + [ + styles.alignItemsCenter, + styles.flexRow, + styles.justifyContentCenter, + styles.flexWrap, + styles.textAlignCenter, + styles.gap1, + styles.quickActionTooltipSubtitle, + theme.tooltipHighlightText, + translate, + ], + ); + const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT; const sidebarInnerRowStyle = StyleSheet.flatten( isInFocusMode @@ -100,7 +133,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti * @param [event] - A press event. */ const showPopover = (event: MouseEvent | GestureResponderEvent) => { - if (!isFocusedRef.current && shouldUseNarrowLayout) { + if (!isScreenFocused && shouldUseNarrowLayout) { return; } setIsContextMenuActive(true); @@ -137,176 +170,199 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti shouldShowErrorMessages={false} needsOffscreenAlphaCompositing > - - {(hovered) => ( - { - Performance.markStart(CONST.TIMING.OPEN_REPORT); + User.dismissGBRTooltip()} + > + + + {(hovered) => ( + { + Performance.markStart(CONST.TIMING.OPEN_REPORT); - event?.preventDefault(); - // Enable Composer to focus on clicking the same chat after opening the context menu. - ReportActionComposeFocusManager.focus(); - onSelectRow(optionItem, popoverAnchor); - }} - onMouseDown={(event) => { - // Allow composer blur on right click - if (!event) { - return; - } + event?.preventDefault(); + // Enable Composer to focus on clicking the same chat after opening the context menu. + ReportActionComposeFocusManager.focus(); + onSelectRow(optionItem, popoverAnchor); + }} + onMouseDown={(event) => { + // Allow composer blur on right click + if (!event) { + return; + } - // Prevent composer blur on left click - event.preventDefault(); - }} - testID={optionItem.reportID} - onSecondaryInteraction={(event) => { - showPopover(event); - // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time - if (DomUtils.getActiveElement()) { - (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); - } - }} - withoutFocusOnSecondaryInteraction - activeOpacity={0.8} - style={[ - styles.flexRow, - styles.alignItemsCenter, - styles.justifyContentBetween, - styles.sidebarLink, - styles.sidebarLinkInnerLHN, - StyleUtils.getBackgroundColorStyle(theme.sidebar), - isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, - ]} - role={CONST.ROLE.BUTTON} - accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - onLayout={onLayout} - needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} - > - - - {!!optionItem.icons?.length && - (optionItem.shouldShowSubscript ? ( - - ) : ( - - ))} - - - - {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( - - )} - {isStatusVisible && ( - - {emojiCode} - + // Prevent composer blur on left click + event.preventDefault(); + }} + testID={optionItem.reportID} + onSecondaryInteraction={(event) => { + showPopover(event); + // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time + if (DomUtils.getActiveElement()) { + (DomUtils.getActiveElement() as HTMLElement | null)?.blur(); + } + }} + withoutFocusOnSecondaryInteraction + activeOpacity={0.8} + style={[ + styles.flexRow, + styles.alignItemsCenter, + styles.justifyContentBetween, + styles.sidebarLink, + styles.sidebarLinkInnerLHN, + StyleUtils.getBackgroundColorStyle(theme.sidebar), + isFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isFocused ? styles.sidebarLinkHover : null, + ]} + role={CONST.ROLE.BUTTON} + accessibilityLabel={translate('accessibilityHints.navigatesToChat')} + onLayout={onLayout} + needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} + > + + + {!!optionItem.icons?.length && + (optionItem.shouldShowSubscript ? ( + + ) : ( + + ))} + + + + {ReportUtils.isChatUsedForOnboarding(report) && SubscriptionUtils.isUserOnFreeTrial() && ( + + )} + {isStatusVisible && ( + + {emojiCode} + + )} + + {optionItem.alternateText ? ( + + {Parser.htmlToText(optionItem.alternateText)} + + ) : null} + + {optionItem?.descriptiveText ? ( + + {optionItem.descriptiveText} + + ) : null} + {hasBrickError && ( + + + )} - {optionItem.alternateText ? ( - - {Parser.htmlToText(optionItem.alternateText)} - - ) : null} - - {optionItem?.descriptiveText ? ( - - {optionItem.descriptiveText} - - ) : null} - {hasBrickError && ( - - - - )} - - - - {shouldShowGreenDotIndicator && ( - - - )} - {hasDraftComment && optionItem.isAllowedToComment && ( - - - )} - {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( - - + {shouldShowGreenDotIndicator && ( + + + + )} + {hasDraftComment && optionItem.isAllowedToComment && ( + + + + )} + {!shouldShowGreenDotIndicator && !hasBrickError && optionItem.isPinned && ( + + + + )} - )} - - - )} - + + )} + + + ); } diff --git a/src/components/LHNOptionsList/OptionRowRendererComponent/index.native.tsx b/src/components/LHNOptionsList/OptionRowRendererComponent/index.native.tsx new file mode 100644 index 0000000000000..ff050f673951e --- /dev/null +++ b/src/components/LHNOptionsList/OptionRowRendererComponent/index.native.tsx @@ -0,0 +1,16 @@ +import {CellContainer} from '@shopify/flash-list'; +import type {CellContainerProps} from '@shopify/flash-list/dist/native/cell-container/CellContainer'; + +function OptionRowRendererComponent(props: CellContainerProps) { + return ( + + ); +} + +OptionRowRendererComponent.displayName = 'OptionRowRendererComponent'; + +export default OptionRowRendererComponent; diff --git a/src/components/LHNOptionsList/OptionRowRendererComponent/index.tsx b/src/components/LHNOptionsList/OptionRowRendererComponent/index.tsx new file mode 100644 index 0000000000000..25afb0124e9f7 --- /dev/null +++ b/src/components/LHNOptionsList/OptionRowRendererComponent/index.tsx @@ -0,0 +1,3 @@ +const OptionRowRendererComponent = undefined; + +export default OptionRowRendererComponent; diff --git a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx index 9ce1dd8138257..62916c483cd67 100644 --- a/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx +++ b/src/components/Tooltip/EducationalTooltip/BaseEducationalTooltip.tsx @@ -1,9 +1,8 @@ -import React, {memo, useEffect, useRef} from 'react'; -import {InteractionManager} from 'react-native'; +import React, {memo, useEffect, useRef, useState} from 'react'; import type {LayoutRectangle, NativeSyntheticEvent} from 'react-native'; import GenericTooltip from '@components/Tooltip/GenericTooltip'; import type {EducationalTooltipProps} from '@components/Tooltip/types'; -import CONST from '@src/CONST'; +import measureTooltipCoordinate from './measureTooltipCoordinate'; type LayoutChangeEventWithTarget = NativeSyntheticEvent<{layout: LayoutRectangle; target: HTMLElement}>; @@ -11,9 +10,12 @@ type LayoutChangeEventWithTarget = NativeSyntheticEvent<{layout: LayoutRectangle * A component used to wrap an element intended for displaying a tooltip. * This tooltip would show immediately without user's interaction and hide after 5 seconds. */ -function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: EducationalTooltipProps) { +function BaseEducationalTooltip({children, shouldAutoDismiss = false, shouldRender = true, ...props}: EducationalTooltipProps) { const hideTooltipRef = useRef<() => void>(); + const [shouldMeasure, setShouldMeasure] = useState(false); + const show = useRef<() => void>(); + useEffect( () => () => { if (!hideTooltipRef.current) { @@ -37,6 +39,16 @@ function BaseEducationalTooltip({children, shouldAutoDismiss = false, ...props}: }; }, [shouldAutoDismiss]); + useEffect(() => { + if (!shouldRender || !shouldMeasure) { + return; + } + // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. + setTimeout(() => { + show.current?.(); + }, 500); + }, [shouldMeasure, shouldRender]); + return ( { + if (!shouldMeasure) { + setShouldMeasure(true); + } // e.target is specific to native, use e.nativeEvent.target on web instead const target = e.target || e.nativeEvent.target; - // When tooltip is used inside an animated view (e.g. popover), we need to wait for the animation to finish before measuring content. - setTimeout(() => { - InteractionManager.runAfterInteractions(() => { - target?.measure((fx, fy, width, height, px, py) => { - updateTargetBounds({ - height, - width, - x: px, - y: py, - }); - showTooltip(); - }); - }); - }, CONST.ANIMATED_TRANSITION); + show.current = () => measureTooltipCoordinate(target, updateTargetBounds, showTooltip); }, }); }} diff --git a/src/components/Tooltip/EducationalTooltip/index.tsx b/src/components/Tooltip/EducationalTooltip/index.tsx index d43ff64d7e8ec..03500f768dd9b 100644 --- a/src/components/Tooltip/EducationalTooltip/index.tsx +++ b/src/components/Tooltip/EducationalTooltip/index.tsx @@ -2,11 +2,7 @@ import React from 'react'; import type {TooltipExtendedProps} from '@components/Tooltip/types'; import BaseEducationalTooltip from './BaseEducationalTooltip'; -function EducationalTooltip({shouldRender = true, children, ...props}: TooltipExtendedProps) { - if (!shouldRender) { - return children; - } - +function EducationalTooltip({children, ...props}: TooltipExtendedProps) { return ( , updateTargetBounds: (rect: LayoutRectangle) => void, showTooltip: () => void) { + return target?.measure((x, y, width, height, px, py) => { + updateTargetBounds({height, width, x: px, y: py}); + showTooltip(); + }); +} diff --git a/src/components/Tooltip/EducationalTooltip/measureTooltipCoordinate/index.ts b/src/components/Tooltip/EducationalTooltip/measureTooltipCoordinate/index.ts new file mode 100644 index 0000000000000..72cc75115e21d --- /dev/null +++ b/src/components/Tooltip/EducationalTooltip/measureTooltipCoordinate/index.ts @@ -0,0 +1,9 @@ +import type React from 'react'; +import type {LayoutRectangle, NativeMethods} from 'react-native'; + +export default function measureTooltipCoordinate(target: React.Component & Readonly, updateTargetBounds: (rect: LayoutRectangle) => void, showTooltip: () => void) { + return target?.measureInWindow((x, y, width, height) => { + updateTargetBounds({height, width, x, y}); + showTooltip(); + }); +} diff --git a/src/components/Tooltip/types.ts b/src/components/Tooltip/types.ts index 4165b960f3229..0462b36fa5249 100644 --- a/src/components/Tooltip/types.ts +++ b/src/components/Tooltip/types.ts @@ -75,6 +75,9 @@ type EducationalTooltipProps = ChildrenProps & SharedTooltipProps & { /** Whether to automatically dismiss the tooltip after 5 seconds */ shouldAutoDismiss?: boolean; + + /** Whether the actual Tooltip should be rendered. If false, it's just going to return the children */ + shouldRender?: boolean; }; type TooltipExtendedProps = (EducationalTooltipProps | TooltipProps) & { diff --git a/src/languages/en.ts b/src/languages/en.ts index 38784f14a77db..06ebbbe998cd9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -660,6 +660,7 @@ export default { listOfChatMessages: 'List of chat messages', listOfChats: 'List of chats', saveTheWorld: 'Save the world', + tooltip: 'Get started here!', }, allSettingsScreen: { subscription: 'Subscription', diff --git a/src/languages/es.ts b/src/languages/es.ts index a56204de6fe9c..1fe4d6025687c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -653,6 +653,7 @@ export default { listOfChatMessages: 'Lista de mensajes del chat', listOfChats: 'lista de chats', saveTheWorld: 'Salvar el mundo', + tooltip: '¡Comienza aquí!', }, allSettingsScreen: { subscription: 'Suscripcion', diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 355dcf93338e2..d2513ce744541 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1307,6 +1307,10 @@ function dismissWorkspaceTooltip() { Onyx.merge(ONYXKEYS.NVP_WORKSPACE_TOOLTIP, {shouldShow: false}); } +function dismissGBRTooltip() { + Onyx.merge(ONYXKEYS.NVP_SHOULD_HIDE_GBR_TOOLTIP, true); +} + function requestRefund() { API.write(WRITE_COMMANDS.REQUEST_REFUND, null); } @@ -1349,4 +1353,5 @@ export { requestValidateCodeAction, addPendingContactMethod, clearValidateCodeActionError, + dismissGBRTooltip, }; diff --git a/src/styles/variables.ts b/src/styles/variables.ts index c0c058352d00b..2a84efc728144 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -251,6 +251,7 @@ export default { composerTooltipShiftHorizontal: 10, composerTooltipShiftVertical: -10, + gbrTooltipShiftHorizontal: -20, h20: 20, h28: 28,