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,