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,