diff --git a/assets/images/customEmoji/global-create.svg b/assets/images/customEmoji/global-create.svg new file mode 100644 index 000000000000..60b46eb97aed --- /dev/null +++ b/assets/images/customEmoji/global-create.svg @@ -0,0 +1,14 @@ + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index 7f56bdd5e370..1250092cb910 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -159,7 +159,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -182,7 +182,7 @@ const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage = '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + @@ -206,7 +206,7 @@ const onboardingPersonalSpendMessage: OnboardingMessage = { '\n' + 'Here’s how to track an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Click "Just track it (don\'t submit it)".\n' + @@ -229,7 +229,7 @@ const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessage = { '\n' + 'Here’s how to track an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Click "Just track it (don\'t submit it)".\n' + @@ -5192,7 +5192,7 @@ const CONST = { '\n' + 'Here’s how to start a chat:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Start chat*.\n' + '3. Enter emails or phone numbers.\n' + '\n' + @@ -5209,7 +5209,7 @@ const CONST = { '\n' + 'Here’s how to request money:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button\n' + '2. Choose *Start chat*.\n' + '3. Enter any email, SMS, or name of who you want to split with.\n' + '4. From within the chat, click the *+* button on the message bar, and click *Split expense*.\n' + @@ -5244,7 +5244,7 @@ const CONST = { '\n' + 'Here’s how to submit an expense:\n' + '\n' + - '1. Click the green *+* button.\n' + + '1. Press the button.\n' + '2. Choose *Create expense*.\n' + '3. Enter an amount or scan a receipt.\n' + '4. Add your reimburser to the request.\n' + diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx index 12b515194928..332255e53995 100755 --- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx +++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx @@ -80,6 +80,7 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim 'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}), 'mention-report': HTMLElementModel.fromCustomModel({tagName: 'mention-report', contentModel: HTMLContentModel.textual}), 'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}), + 'custom-emoji': HTMLElementModel.fromCustomModel({tagName: 'custom-emoji', contentModel: HTMLContentModel.textual}), 'next-step': HTMLElementModel.fromCustomModel({ tagName: 'next-step', mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16}, diff --git a/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx b/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx new file mode 100644 index 000000000000..8cd33eab6c90 --- /dev/null +++ b/src/components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction.tsx @@ -0,0 +1,21 @@ +import type {ReactNode} from 'react'; +import React from 'react'; +import FloatingActionButtonAndPopover from '@pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover'; + +type CustomEmojiWithDefaultPressableActionProps = { + /* Key name identifying the emoji */ + emojiKey: string; + + /* Emoji content to render */ + children: ReactNode; +}; + +function CustomEmojiWithDefaultPressableAction({emojiKey, children}: CustomEmojiWithDefaultPressableActionProps) { + if (emojiKey === 'actionMenuIcon') { + return {children}; + } + + return children; +} + +export default CustomEmojiWithDefaultPressableAction; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx new file mode 100644 index 000000000000..dab8c89013dd --- /dev/null +++ b/src/components/HTMLEngineProvider/HTMLRenderers/CustomEmojiRenderer.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import type {FC} from 'react'; +import {View} from 'react-native'; +import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; +import type {SvgProps} from 'react-native-svg'; +import GlobalCreateIcon from '@assets/images/customEmoji/global-create.svg'; +import CustomEmojiWithDefaultPressableAction from '@components/HTMLEngineProvider/CustomEmojiWithDefaultPressableAction'; +import ImageSVG from '@components/ImageSVG'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; + +const emojiMap: Record> = { + actionMenuIcon: GlobalCreateIcon, +}; + +function CustomEmojiRenderer({tnode}: CustomRendererProps) { + const styles = useThemeStyles(); + const emojiKey = tnode.attributes.emoji; + + if (emojiMap[emojiKey]) { + const image = ( + + + + ); + + if ('pressablewithdefaultaction' in tnode.attributes) { + return {image}; + } + + return image; + } + + return null; +} + +export default CustomEmojiRenderer; +export {emojiMap}; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts index 91ed66f8b931..bcf3d4dfaf94 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/index.ts +++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.ts @@ -1,6 +1,7 @@ import type {CustomTagRendererRecord} from 'react-native-render-html'; import AnchorRenderer from './AnchorRenderer'; import CodeRenderer from './CodeRenderer'; +import CustomEmojiRenderer from './CustomEmojiRenderer'; import DeletedActionRenderer from './DeletedActionRenderer'; import EditedRenderer from './EditedRenderer'; import EmojiRenderer from './EmojiRenderer'; @@ -29,6 +30,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = { 'mention-user': MentionUserRenderer, 'mention-report': MentionReportRenderer, 'mention-here': MentionHereRenderer, + 'custom-emoji': CustomEmojiRenderer, emoji: EmojiRenderer, 'next-step-email': NextStepEmailRenderer, 'deleted-action': DeletedActionRenderer, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6033ad6a6cfe..f91df123c2a1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -6448,6 +6448,7 @@ function buildOptimisticTaskReport( description?: string, policyID: string = CONST.POLICY.OWNER_EMAIL_FAKE, notificationPreference: NotificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + shouldEscapeText = true, ): OptimisticTaskReport { const participants: Participants = { [ownerAccountID]: { @@ -6462,7 +6463,7 @@ function buildOptimisticTaskReport( return { reportID: generateReportID(), reportName: title, - description: getParsedComment(description ?? ''), + description: getParsedComment(description ?? '', {shouldEscapeText}), ownerAccountID, participants, managerID: assigneeAccountID, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e120ce1c21fa..b77b8115cb59 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3731,6 +3731,7 @@ function prepareOnboardingOnyxData( taskDescription, targetChatPolicyID, CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + false, ); const emailCreatingAction = engagementChoice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ? allPersonalDetails?.[actorAccountID]?.login ?? CONST.EMAIL.CONCIERGE : CONST.EMAIL.CONCIERGE; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 2085cc181aaa..c8c9ae12773d 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -30,7 +30,6 @@ import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {isOffline} from '@libs/Network/NetworkStore'; import * as SequentialQueue from '@libs/Network/SequentialQueue'; -import NetworkConnection from '@libs/NetworkConnection'; import * as NumberUtils from '@libs/NumberUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as Pusher from '@libs/Pusher/pusher'; @@ -972,7 +971,6 @@ function checkforMissingPongEvents() { // If the oldest timestamp is older than 2 * PING_INTERVAL_LENGTH_IN_SECONDS, then set the network status to offline if (ageOfEventInMS > NO_EVENT_RECEIVED_TO_BE_OFFLINE_THRESHOLD_IN_SECONDS * 1000) { Log.info(`[Pusher PINGPONG] The server has not replied to the PING event ${eventID} in ${ageOfEventInMS} ms so going offline`); - // NetworkConnection.setOfflineStatus(true, 'The client never got a Pusher PONG event after sending a Pusher PING event'); // When going offline, reset the pingpong state so that when the network reconnects, the client will start fresh lastTimestamp = Date.now(); diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx index b77e1bd89f67..ca90fc45997e 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.tsx @@ -1,6 +1,6 @@ import {useIsFocused as useIsFocusedOriginal, useNavigationState} from '@react-navigation/native'; import type {ImageContentFit} from 'expo-image'; -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, ReactNode} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -11,6 +11,7 @@ import FloatingActionButton from '@components/FloatingActionButton'; import * as Expensicons from '@components/Icon/Expensicons'; import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; +import {PressableWithoutFeedback} from '@components/Pressable'; import {useProductTrainingContext} from '@components/ProductTrainingContext'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; @@ -67,6 +68,12 @@ type FloatingActionButtonAndPopoverProps = { /* Callback function before the menu is hidden */ onHideCreateMenu?: () => void; + + /* Render the FAB as an emoji */ + isEmoji?: boolean; + + /* Emoji content to render when isEmoji */ + children?: ReactNode; }; type FloatingActionButtonAndPopoverRef = { @@ -166,7 +173,7 @@ const getQuickActionTitle = (action: QuickActionName): TranslationPaths => { * Responsible for rendering the {@link PopoverMenu}, and the accompanying * FAB that can open or close the menu. */ -function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) { +function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isEmoji, children}: FloatingActionButtonAndPopoverProps, ref: ForwardedRef) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); @@ -462,102 +469,130 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu}: Fl const canModifyTask = canModifyTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID); const canActionTask = canActionTaskUtils(viewTourTaskReport, currentUserPersonalDetails.accountID); - return ( - - interceptAnonymousUser(startNewChat), - }, - ...(canSendInvoice - ? [ - { - icon: Expensicons.InvoiceGeneric, - text: translate('workspace.invoices.sendInvoice'), - shouldCallAfterModalHide: shouldRedirectToExpensifyClassic, - onSelected: () => - interceptAnonymousUser(() => { - if (shouldRedirectToExpensifyClassic) { - setModalVisible(true); - return; - } - - startMoneyRequest( - CONST.IOU.TYPE.INVOICE, - // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used - // for all of the routes in the creation flow. - generateReportID(), - ); - }), - }, - ] - : []), - ...(canUseSpotnanaTravel - ? [ - { - icon: Expensicons.Suitcase, - text: translate('travel.bookTravel'), - onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)), - }, - ] - : []), - ...(!hasSeenTour - ? [ - { - icon: Expensicons.Binoculars, - iconStyles: styles.popoverIconCircle, - iconFill: theme.icon, - text: translate('tour.takeATwoMinuteTour'), - description: translate('tour.exploreExpensify'), - onSelected: () => { - openExternalLink(navatticURL); - setSelfTourViewed(isAnonymousUser()); - if (viewTourTaskReport && canModifyTask && canActionTask) { - completeTask(viewTourTaskReport); + const popoverMenu = ( + interceptAnonymousUser(startNewChat), + }, + ...(canSendInvoice + ? [ + { + icon: Expensicons.InvoiceGeneric, + text: translate('workspace.invoices.sendInvoice'), + shouldCallAfterModalHide: shouldRedirectToExpensifyClassic, + onSelected: () => + interceptAnonymousUser(() => { + if (shouldRedirectToExpensifyClassic) { + setModalVisible(true); + return; } - }, - }, - ] - : []), - ...(!isLoading && shouldShowNewWorkspaceButton - ? [ - { - displayInDefaultIconColor: true, - contentFit: 'contain' as ImageContentFit, - icon: Expensicons.NewWorkspace, - iconWidth: variables.w46, - iconHeight: variables.h40, - text: translate('workspace.new.newWorkspace'), - description: translate('workspace.new.getTheExpensifyCardAndMore'), - onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), + + startMoneyRequest( + CONST.IOU.TYPE.INVOICE, + // When starting to create an invoice from the global FAB, there is not an existing report yet. A random optimistic reportID is generated and used + // for all of the routes in the creation flow. + generateReportID(), + ); + }), + }, + ] + : []), + ...(canUseSpotnanaTravel + ? [ + { + icon: Expensicons.Suitcase, + text: translate('travel.bookTravel'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.TRAVEL_MY_TRIPS)), + }, + ] + : []), + ...(!hasSeenTour + ? [ + { + icon: Expensicons.Binoculars, + iconStyles: styles.popoverIconCircle, + iconFill: theme.icon, + text: translate('tour.takeATwoMinuteTour'), + description: translate('tour.exploreExpensify'), + onSelected: () => { + openExternalLink(navatticURL); + setSelfTourViewed(isAnonymousUser()); + if (viewTourTaskReport && canModifyTask && canActionTask) { + completeTask(viewTourTaskReport); + } }, - ] - : []), - ...quickActionMenuItems, - ]} - withoutOverlay - anchorRef={fabRef} - /> - { - setModalVisible(false); - openOldDotLink(CONST.OLDDOT_URLS.INBOX); - }} - onCancel={() => setModalVisible(false)} - title={translate('sidebarScreen.redirectToExpensifyClassicModal.title')} - confirmText={translate('exitSurvey.goToExpensifyClassic')} - cancelText={translate('common.cancel')} - /> + }, + ] + : []), + ...(!isLoading && shouldShowNewWorkspaceButton + ? [ + { + displayInDefaultIconColor: true, + contentFit: 'contain' as ImageContentFit, + icon: Expensicons.NewWorkspace, + iconWidth: variables.w46, + iconHeight: variables.h40, + text: translate('workspace.new.newWorkspace'), + description: translate('workspace.new.getTheExpensifyCardAndMore'), + onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(Navigation.getActiveRoute()))), + }, + ] + : []), + ...quickActionMenuItems, + ]} + withoutOverlay + anchorRef={fabRef} + /> + ); + + const confirmModal = ( + { + setModalVisible(false); + openOldDotLink(CONST.OLDDOT_URLS.INBOX); + }} + onCancel={() => setModalVisible(false)} + title={translate('sidebarScreen.redirectToExpensifyClassicModal.title')} + confirmText={translate('exitSurvey.goToExpensifyClassic')} + cancelText={translate('common.cancel')} + /> + ); + + if (isEmoji) { + return ( + <> + + {popoverMenu} + {confirmModal} + + + {children} + + + ); + } + + return ( + + {popoverMenu} + {confirmModal} justifyContent: 'center', }, + customEmoji: { + alignItems: 'center', + justifyContent: 'center', + verticalAlign: 'bottom', + ...(getPlatform() === CONST.PLATFORM.IOS || getPlatform() === CONST.PLATFORM.ANDROID ? {marginBottom: -variables.iconSizeNormal / 4} : {}), + }, + sidebarFooterUsername: { color: theme.heading, fontSize: variables.fontSizeLabel,