From 3f019096d3186ac938f383549eb35ca06474ba21 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 6 May 2024 18:35:27 +0700 Subject: [PATCH 001/192] fix: tapping assignee and mark as complete quickly navigates to not found page --- src/components/MenuItem.tsx | 10 +- src/components/TaskHeaderActionButton.tsx | 15 ++- src/pages/home/ReportScreen.tsx | 147 +++++++++++---------- src/pages/settings/Profile/ProfilePage.tsx | 7 +- 4 files changed, 95 insertions(+), 84 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index bf35d65340fc..10d2b72a0ab9 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -342,7 +342,7 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); - const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; + const {isExecuting, singleExecution} = useContext(MenuItemGroupContext) ?? {}; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; @@ -417,15 +417,11 @@ function MenuItem( } if (onPress && event) { - if (!singleExecution || !waitForNavigate) { + if (!singleExecution) { onPress(event); return; } - singleExecution( - waitForNavigate(() => { - onPress(event); - }), - )(); + singleExecution(onPress)(event); } }; diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 788734242f7b..b9aaf562b471 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useContext} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -10,6 +10,7 @@ import * as Task from '@userActions/Task'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import Button from './Button'; +import {MenuItemGroupContext} from './MenuItemGroup'; type TaskHeaderActionButtonOnyxProps = { /** Current user session */ @@ -24,6 +25,16 @@ type TaskHeaderActionButtonProps = TaskHeaderActionButtonOnyxProps & { function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {singleExecution} = useContext(MenuItemGroupContext) ?? {}; + + const onPressAction = () => { + const onPress = () => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)); + if (!singleExecution) { + onPress(); + return; + } + singleExecution(onPress)(); + }; if (!ReportUtils.canWriteInReport(report)) { return null; @@ -36,7 +47,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) isDisabled={!Task.canModifyTask(report, session?.accountID ?? 0)} medium text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} - onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))} + onPress={Session.checkIfActionIsAllowed(onPressAction)} style={styles.flex1} /> diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b82137756a28..743d32c38009 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -12,6 +12,7 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import * as Illustrations from '@components/Icon/Illustrations'; +import MenuItemGroup from '@components/MenuItemGroup'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -651,85 +652,87 @@ function ReportScreen({ return ( - - + - - {headerView} - {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( - - - - + + {headerView} + {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( + + + + + - - )} - - {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( - - )} - - - {shouldShowReportActionList && ( - )} - - {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. + + {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + + )} + + + {shouldShowReportActionList && ( + + )} + + {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {shouldShowSkeleton && } - - {isCurrentReportLoadedFromOnyx ? ( - setIsComposerFocus(true)} - onComposerBlur={() => setIsComposerFocus(false)} - report={report} - pendingAction={reportPendingAction} - isComposerFullSize={!!isComposerFullSize} - listHeight={listHeight} - isEmptyChat={isEmptyChat} - lastReportAction={lastReportAction} - /> - ) : null} - - - - + {shouldShowSkeleton && } + + {isCurrentReportLoadedFromOnyx ? ( + setIsComposerFocus(true)} + onComposerBlur={() => setIsComposerFocus(false)} + report={report} + pendingAction={reportPendingAction} + isComposerFullSize={!!isComposerFullSize} + listHeight={listHeight} + isEmptyChat={isEmptyChat} + lastReportAction={lastReportAction} + /> + ) : null} + + + + + ); diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 4c5ed88e6898..f33a86ed2d46 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -1,11 +1,11 @@ -import React, {useEffect} from 'react'; +import React, {useContext, useEffect} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; -import MenuItemGroup from '@components/MenuItemGroup'; +import MenuItemGroup, {MenuItemGroupContext} from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -61,6 +61,7 @@ function ProfilePage({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); + const {waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const getPronouns = (): string => { const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; @@ -179,7 +180,7 @@ function ProfilePage({ title={detail.title} description={detail.description} wrapperStyle={styles.sectionMenuItemTopDescription} - onPress={() => Navigation.navigate(detail.pageRoute)} + onPress={waitForNavigate ? waitForNavigate(() => Navigation.navigate(detail.pageRoute)) : () => Navigation.navigate(detail.pageRoute)} /> ))} From c2cbc58d153f12fb88609915241ad82a88d809c5 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 15 May 2024 12:35:19 -0700 Subject: [PATCH 002/192] if name doesn't exist in a frequentlyUsedEmoji, retrieve it from the assets --- src/libs/EmojiUtils.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 3b189dbb084f..2e1919dfb6a0 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -208,7 +208,16 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL return addSpacesToEmojiCategories(emojis); } - const mergedEmojis = [Emojis.categoryFrequentlyUsed, ...frequentlyUsedEmojis, ...emojis]; + const formattedFrequentlyUsedEmojis = frequentlyUsedEmojis.map((frequentlyUsedEmoji: Emoji): Emoji => { + // Frequently used emojis in the old format will have name/types/code stored with them + // In the new format, only the code is stored, so we'll need to retrieve the name/types + if (!('name' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { + return findEmojiByCode(frequentlyUsedEmoji.code); + } + return frequentlyUsedEmoji; + }); + + const mergedEmojis = [Emojis.categoryFrequentlyUsed, ...formattedFrequentlyUsedEmojis, ...emojis]; return addSpacesToEmojiCategories(mergedEmojis); } From e1642e3fc9d64f4a9a5d3d41dd8647c0da7b01ce Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Wed, 15 May 2024 12:38:50 -0700 Subject: [PATCH 003/192] remove all calls to UpdateFrequentlyUsedEmojis --- .../UpdateFrequentlyUsedEmojisParams.ts | 3 - src/libs/API/parameters/index.ts | 1 - src/libs/API/types.ts | 2 - src/libs/actions/User.ts | 19 - .../ComposerWithSuggestions.tsx | 11 - .../report/ReportActionItemMessageEdit.tsx | 16 +- tests/unit/EmojiTest.ts | 324 ------------------ 7 files changed, 1 insertion(+), 375 deletions(-) delete mode 100644 src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts diff --git a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts b/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts deleted file mode 100644 index f790ada3aad9..000000000000 --- a/src/libs/API/parameters/UpdateFrequentlyUsedEmojisParams.ts +++ /dev/null @@ -1,3 +0,0 @@ -type UpdateFrequentlyUsedEmojisParams = {value: string}; - -export default UpdateFrequentlyUsedEmojisParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 29044d4fac16..c2249fdcdaf3 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -61,7 +61,6 @@ export type {default as UpdateAutomaticTimezoneParams} from './UpdateAutomaticTi export type {default as UpdateChatPriorityModeParams} from './UpdateChatPriorityModeParams'; export type {default as UpdateDateOfBirthParams} from './UpdateDateOfBirthParams'; export type {default as UpdateDisplayNameParams} from './UpdateDisplayNameParams'; -export type {default as UpdateFrequentlyUsedEmojisParams} from './UpdateFrequentlyUsedEmojisParams'; export type {default as UpdateGroupChatNameParams} from './UpdateGroupChatNameParams'; export type {default as UpdateGroupChatMemberRolesParams} from './UpdateGroupChatMemberRolesParams'; export type {default as UpdateHomeAddressParams} from './UpdateHomeAddressParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8fe704ff220e..21e31fed9a53 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -58,7 +58,6 @@ const WRITE_COMMANDS = { VALIDATE_LOGIN: 'ValidateLogin', VALIDATE_SECONDARY_LOGIN: 'ValidateSecondaryLogin', UPDATE_PREFERRED_EMOJI_SKIN_TONE: 'UpdatePreferredEmojiSkinTone', - UPDATE_FREQUENTLY_USED_EMOJIS: 'UpdateFrequentlyUsedEmojis', UPDATE_CHAT_PRIORITY_MODE: 'UpdateChatPriorityMode', SET_CONTACT_METHOD_AS_DEFAULT: 'SetContactMethodAsDefault', UPDATE_THEME: 'UpdateTheme', @@ -263,7 +262,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.VALIDATE_LOGIN]: Parameters.ValidateLoginParams; [WRITE_COMMANDS.VALIDATE_SECONDARY_LOGIN]: Parameters.ValidateSecondaryLoginParams; [WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE]: Parameters.UpdatePreferredEmojiSkinToneParams; - [WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS]: Parameters.UpdateFrequentlyUsedEmojisParams; [WRITE_COMMANDS.UPDATE_CHAT_PRIORITY_MODE]: Parameters.UpdateChatPriorityModeParams; [WRITE_COMMANDS.SET_CONTACT_METHOD_AS_DEFAULT]: Parameters.SetContactMethodAsDefaultParams; [WRITE_COMMANDS.UPDATE_THEME]: Parameters.UpdateThemeParams; diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index f7e90f775b65..d68f14aae5f2 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -13,7 +13,6 @@ import type { SetContactMethodAsDefaultParams, SetNameValuePairParams, UpdateChatPriorityModeParams, - UpdateFrequentlyUsedEmojisParams, UpdateNewsletterSubscriptionParams, UpdatePreferredEmojiSkinToneParams, UpdateStatusParams, @@ -644,23 +643,6 @@ function updatePreferredSkinTone(skinTone: number) { API.write(WRITE_COMMANDS.UPDATE_PREFERRED_EMOJI_SKIN_TONE, parameters, {optimisticData}); } -/** - * Sync frequentlyUsedEmojis with Onyx and Server - */ -function updateFrequentlyUsedEmojis(frequentlyUsedEmojis: FrequentlyUsedEmoji[]) { - const optimisticData: OnyxUpdate[] = [ - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, - value: frequentlyUsedEmojis, - }, - ]; - - const parameters: UpdateFrequentlyUsedEmojisParams = {value: JSON.stringify(frequentlyUsedEmojis)}; - - API.write(WRITE_COMMANDS.UPDATE_FREQUENTLY_USED_EMOJIS, parameters, {optimisticData}); -} - /** * Sync user chat priority mode with Onyx and Server * @param mode @@ -1030,7 +1012,6 @@ export { setShouldUseStagingServer, setMuteAllSounds, clearUserErrorMessage, - updateFrequentlyUsedEmojis, joinScreenShare, clearScreenShareRequest, generateStatementPDF, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 3120bbe9bed2..669f773f4ed7 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -304,15 +304,6 @@ function ComposerWithSuggestions( // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); - /** - * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis - * API is not called too often. - */ - const debouncedUpdateFrequentlyUsedEmojis = useCallback(() => { - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(insertedEmojisRef.current)); - insertedEmojisRef.current = []; - }, []); - /** * Set the TextInput Ref */ @@ -419,7 +410,6 @@ function ComposerWithSuggestions( suggestionsRef.current.resetSuggestions(); } insertedEmojisRef.current = [...insertedEmojisRef.current, ...newEmojis]; - debouncedUpdateFrequentlyUsedEmojis(); } } const newCommentConverted = convertToLTRForComposer(newComment); @@ -457,7 +447,6 @@ function ComposerWithSuggestions( } }, [ - debouncedUpdateFrequentlyUsedEmojis, findNewlyAddedChars, preferredLocale, preferredSkinTone, diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index eda4ef5b1033..6b7acda63375 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -210,19 +210,6 @@ function ReportActionItemMessageEdit( [debouncedSaveDraft], ); - /** - * Update frequently used emojis list. We debounce this method in the constructor so that UpdateFrequentlyUsedEmojis - * API is not called too often. - */ - const debouncedUpdateFrequentlyUsedEmojis = useMemo( - () => - lodashDebounce(() => { - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(insertedEmojis.current)); - insertedEmojis.current = []; - }, 1000), - [], - ); - /** * Update the value of the draft in Onyx * @@ -236,7 +223,6 @@ function ReportActionItemMessageEdit( const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis?.length > 0) { insertedEmojis.current = [...insertedEmojis.current, ...newEmojis]; - debouncedUpdateFrequentlyUsedEmojis(); } } emojisPresentBefore.current = emojis; @@ -257,7 +243,7 @@ function ReportActionItemMessageEdit( debouncedSaveDraft(newDraft); isCommentPendingSaved.current = true; }, - [debouncedSaveDraft, debouncedUpdateFrequentlyUsedEmojis, preferredSkinTone, preferredLocale, selection.end], + [debouncedSaveDraft, preferredSkinTone, preferredLocale, selection.end], ); useEffect(() => { diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index 24e56ec8de23..fe069061d7a3 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -196,328 +196,4 @@ describe('EmojiTest', () => { }, ]); }); - - describe('update frequently used emojis', () => { - let spy: jest.SpyInstance; - - beforeAll(() => { - Onyx.init({keys: ONYXKEYS}); - // @ts-expect-error TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated to TypeScript. - global.fetch = TestHelper.getGlobalFetchMock(); - spy = jest.spyOn(User, 'updateFrequentlyUsedEmojis'); - }); - - beforeEach(() => { - spy.mockClear(); - return Onyx.clear(); - }); - - it('should put a less frequent and recent used emoji behind', () => { - // Given an existing frequently used emojis list with count > 1 - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '👋', - name: 'wave', - count: 2, - lastUpdatedAt: 4, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - { - code: '💤', - name: 'zzz', - count: 2, - lastUpdatedAt: 3, - }, - { - code: '💯', - name: '100', - count: 2, - lastUpdatedAt: 2, - }, - { - code: '👿', - name: 'imp', - count: 2, - lastUpdatedAt: 1, - }, - ]; - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add a new emoji - const currentTime = getUnixTime(new Date()); - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const newEmoji = [smileEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the new emoji should be at the last item of the list - const expectedSmileEmoji: FrequentlyUsedEmoji = {...smileEmoji, count: 1, lastUpdatedAt: currentTime}; - - const expectedFrequentlyEmojisList = [...frequentlyEmojisList, expectedSmileEmoji]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - - it('should put more frequent and recent used emoji to the front', () => { - // Given an existing frequently used emojis list - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '😠', - name: 'angry', - count: 3, - lastUpdatedAt: 5, - }, - { - code: '👋', - name: 'wave', - count: 2, - lastUpdatedAt: 4, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - { - code: '💤', - name: 'zzz', - count: 2, - lastUpdatedAt: 3, - }, - { - code: '💯', - name: '100', - count: 1, - lastUpdatedAt: 2, - }, - {...smileEmoji, count: 1, lastUpdatedAt: 1}, - ]; - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add an emoji that exists in the list - const currentTime = getUnixTime(new Date()); - const newEmoji = [smileEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the count should be increased and put into the very front of the other emoji within the same count - const expectedFrequentlyEmojisList = [frequentlyEmojisList[0], {...smileEmoji, count: 2, lastUpdatedAt: currentTime}, ...frequentlyEmojisList.slice(1, -1)]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - - it('should sorted descending by count and lastUpdatedAt for multiple emoji added', () => { - // Given an existing frequently used emojis list - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const zzzEmoji: Emoji = {code: '💤', name: 'zzz'}; - const impEmoji: Emoji = {code: '👿', name: 'imp'}; - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '😠', - name: 'angry', - count: 3, - lastUpdatedAt: 5, - }, - { - code: '👋', - name: 'wave', - count: 2, - lastUpdatedAt: 4, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - {...zzzEmoji, count: 2, lastUpdatedAt: 3}, - { - code: '💯', - name: '100', - count: 1, - lastUpdatedAt: 2, - }, - {...smileEmoji, count: 1, lastUpdatedAt: 1}, - ]; - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add multiple emojis that either exist or not exist in the list - const currentTime = getUnixTime(new Date()); - const newEmoji = [smileEmoji, zzzEmoji, impEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the count should be increased for existing emoji and sorted descending by count and lastUpdatedAt - const expectedFrequentlyEmojisList = [ - {...zzzEmoji, count: 3, lastUpdatedAt: currentTime}, - frequentlyEmojisList[0], - {...smileEmoji, count: 2, lastUpdatedAt: currentTime}, - frequentlyEmojisList[1], - {...impEmoji, count: 1, lastUpdatedAt: currentTime}, - frequentlyEmojisList[3], - ]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - - it('make sure the most recent new emoji is added to the list even it is full with count > 1', () => { - // Given an existing full (24 items) frequently used emojis list - const smileEmoji: Emoji = {code: '😄', name: 'smile'}; - const zzzEmoji: Emoji = {code: '💤', name: 'zzz'}; - const impEmoji: Emoji = {code: '👿', name: 'imp'}; - const bookEmoji: Emoji = {code: '📚', name: 'books'}; - const frequentlyEmojisList: FrequentlyUsedEmoji[] = [ - { - code: '😠', - name: 'angry', - count: 3, - lastUpdatedAt: 24, - }, - { - code: '👋', - name: 'wave', - count: 3, - lastUpdatedAt: 23, - types: ['👋🏿', '👋🏾', '👋🏽', '👋🏼', '👋🏻'], - }, - { - code: '😡', - name: 'rage', - count: 3, - lastUpdatedAt: 22, - }, - { - code: '😤', - name: 'triumph', - count: 3, - lastUpdatedAt: 21, - }, - { - code: '🥱', - name: 'yawning_face', - count: 3, - lastUpdatedAt: 20, - }, - { - code: '😫', - name: 'tired_face', - count: 3, - lastUpdatedAt: 19, - }, - { - code: '😩', - name: 'weary', - count: 3, - lastUpdatedAt: 18, - }, - { - code: '😓', - name: 'sweat', - count: 3, - lastUpdatedAt: 17, - }, - { - code: '😞', - name: 'disappointed', - count: 3, - lastUpdatedAt: 16, - }, - { - code: '😣', - name: 'persevere', - count: 3, - lastUpdatedAt: 15, - }, - { - code: '😖', - name: 'confounded', - count: 3, - lastUpdatedAt: 14, - }, - { - code: '👶', - name: 'baby', - count: 3, - lastUpdatedAt: 13, - types: ['👶🏿', '👶🏾', '👶🏽', '👶🏼', '👶🏻'], - }, - { - code: '👄', - name: 'lips', - count: 3, - lastUpdatedAt: 12, - }, - { - code: '🐶', - name: 'dog', - count: 3, - lastUpdatedAt: 11, - }, - { - code: '🦮', - name: 'guide_dog', - count: 3, - lastUpdatedAt: 10, - }, - { - code: '🐱', - name: 'cat', - count: 3, - lastUpdatedAt: 9, - }, - { - code: '🐈‍⬛', - name: 'black_cat', - count: 3, - lastUpdatedAt: 8, - }, - { - code: '🕞', - name: 'clock330', - count: 3, - lastUpdatedAt: 7, - }, - { - code: '🥎', - name: 'softball', - count: 3, - lastUpdatedAt: 6, - }, - { - code: '🏀', - name: 'basketball', - count: 3, - lastUpdatedAt: 5, - }, - { - code: '📟', - name: 'pager', - count: 3, - lastUpdatedAt: 4, - }, - { - code: '🎬', - name: 'clapper', - count: 3, - lastUpdatedAt: 3, - }, - { - code: '📺', - name: 'tv', - count: 3, - lastUpdatedAt: 2, - }, - {...bookEmoji, count: 3, lastUpdatedAt: 1}, - ]; - expect(frequentlyEmojisList.length).toBe(CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW); - Onyx.merge(ONYXKEYS.FREQUENTLY_USED_EMOJIS, frequentlyEmojisList); - - return waitForBatchedUpdates().then(() => { - // When add new emojis - const currentTime = getUnixTime(new Date()); - const newEmoji = [bookEmoji, smileEmoji, zzzEmoji, impEmoji, smileEmoji]; - User.updateFrequentlyUsedEmojis(EmojiUtils.getFrequentlyUsedEmojis(newEmoji)); - - // Then the last emojis from the list should be replaced with the most recent new emoji (smile) - const expectedFrequentlyEmojisList = [ - {...bookEmoji, count: 4, lastUpdatedAt: currentTime}, - ...frequentlyEmojisList.slice(0, -2), - {...smileEmoji, count: 1, lastUpdatedAt: currentTime}, - ]; - expect(spy).toBeCalledWith(expectedFrequentlyEmojisList); - }); - }); - }); }); From accadd6d64a2d9ac6f0d28c0d0bdc7ce88d39022 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 28 May 2024 10:55:15 -0700 Subject: [PATCH 004/192] use new format for frequently used emojis, remove unused --- src/libs/EmojiUtils.ts | 38 +++----------------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 2e1919dfb6a0..2486d67f2445 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -210,9 +210,9 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL const formattedFrequentlyUsedEmojis = frequentlyUsedEmojis.map((frequentlyUsedEmoji: Emoji): Emoji => { // Frequently used emojis in the old format will have name/types/code stored with them - // In the new format, only the code is stored, so we'll need to retrieve the name/types - if (!('name' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { - return findEmojiByCode(frequentlyUsedEmoji.code); + // In the new format, only the name is stored, so we'll need to retrieve the code/types + if (!('code' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { + return findEmojiByName(frequentlyUsedEmoji.name); } return frequentlyUsedEmoji; }); @@ -221,37 +221,6 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL return addSpacesToEmojiCategories(mergedEmojis); } -/** - * Get the updated frequently used emojis list by usage - */ -function getFrequentlyUsedEmojis(newEmoji: Emoji | Emoji[]): FrequentlyUsedEmoji[] { - let frequentEmojiList = [...frequentlyUsedEmojis]; - - const maxFrequentEmojiCount = CONST.EMOJI_FREQUENT_ROW_COUNT * CONST.EMOJI_NUM_PER_ROW - 1; - - const currentTimestamp = getUnixTime(new Date()); - (Array.isArray(newEmoji) ? [...newEmoji] : [newEmoji]).forEach((emoji) => { - let currentEmojiCount = 1; - const emojiIndex = frequentEmojiList.findIndex((e) => e.code === emoji.code); - if (emojiIndex >= 0) { - currentEmojiCount = frequentEmojiList[emojiIndex].count + 1; - frequentEmojiList.splice(emojiIndex, 1); - } - - const updatedEmoji = {...Emojis.emojiCodeTableWithSkinTones[emoji.code], count: currentEmojiCount, lastUpdatedAt: currentTimestamp}; - - // We want to make sure the current emoji is added to the list - // Hence, we take one less than the current frequent used emojis - frequentEmojiList = frequentEmojiList.slice(0, maxFrequentEmojiCount); - frequentEmojiList.push(updatedEmoji); - - // Sort the list by count and lastUpdatedAt in descending order - frequentEmojiList.sort((a, b) => b.count - a.count || b.lastUpdatedAt - a.lastUpdatedAt); - }); - - return frequentEmojiList; -} - /** * Given an emoji item object, return an emoji code based on its type. */ @@ -587,7 +556,6 @@ export { getLocalizedEmojiName, getHeaderEmojis, mergeEmojisWithFrequentlyUsedEmojis, - getFrequentlyUsedEmojis, containsOnlyEmojis, replaceEmojis, suggestEmojis, From 5858bf97d32516a48e9b6b39154b47c6cd7c0e69 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 28 May 2024 15:20:02 -0700 Subject: [PATCH 005/192] fill in Emoji code/name --- src/libs/EmojiUtils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 9e4a0d07f626..33f89f93b118 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -212,10 +212,13 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL const formattedFrequentlyUsedEmojis = frequentlyUsedEmojis.map((frequentlyUsedEmoji: Emoji): Emoji => { // Frequently used emojis in the old format will have name/types/code stored with them - // In the new format, only the name is stored, so we'll need to retrieve the code/types + // The back-end may not always have both, so we'll need to fill them in. if (!('code' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { return findEmojiByName(frequentlyUsedEmoji.name); } + if (!('name' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { + return findEmojiByCode(frequentlyUsedEmoji.code); + } return frequentlyUsedEmoji; }); From 992dede16651311ac037f2e5b101bfe7c75c8a63 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 3 Jun 2024 15:45:13 -0700 Subject: [PATCH 006/192] support new frequentlyUsedEmojis NVP format --- src/libs/EmojiUtils.ts | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 3a9156222a44..d1ae12d4b0f8 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -19,6 +19,10 @@ type EmojiPickerListItem = EmojiSpacer | Emoji | HeaderEmoji; type EmojiPickerList = EmojiPickerListItem[]; type ReplacedEmoji = {text: string; emojis: Emoji[]; cursorPosition?: number}; +const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name]; + +const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code]; + let frequentlyUsedEmojis: FrequentlyUsedEmoji[] = []; Onyx.connect({ key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, @@ -29,16 +33,20 @@ Onyx.connect({ frequentlyUsedEmojis = val ?.map((item) => { - const emoji = Emojis.emojiCodeTableWithSkinTones[item.code]; - return {...emoji, count: item.count, lastUpdatedAt: item.lastUpdatedAt}; + let emoji = item; + if (!item.code) { + emoji = {...emoji, ...findEmojiByName(item.name)}; + } + if (!item.name) { + emoji = {...emoji, ...findEmojiByCode(item.code)}; + } + const emojiWithSkinTones = Emojis.emojiCodeTableWithSkinTones[emoji.code]; + return {...emojiWithSkinTones, count: item.count, lastUpdatedAt: item.lastUpdatedAt}; }) .filter((emoji): emoji is FrequentlyUsedEmoji => !!emoji) ?? []; }, }); -const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name]; - -const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code]; const getEmojiName = (emoji: Emoji, lang: Locale = CONST.LOCALES.DEFAULT): string => { if (!emoji) { @@ -210,19 +218,27 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL return addSpacesToEmojiCategories(emojis); } + console.log(">>>> nvp", frequentlyUsedEmojis); + const formattedFrequentlyUsedEmojis = frequentlyUsedEmojis.map((frequentlyUsedEmoji: Emoji): Emoji => { + console.log(">>>> processing", frequentlyUsedEmoji); // Frequently used emojis in the old format will have name/types/code stored with them // The back-end may not always have both, so we'll need to fill them in. if (!('code' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { + console.log(">>>> code does not exist"); return findEmojiByName(frequentlyUsedEmoji.name); } if (!('name' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { + console.log(">>>> name does not exist"); return findEmojiByCode(frequentlyUsedEmoji.code); } + return frequentlyUsedEmoji; }); + console.log(">>>> formatted", formattedFrequentlyUsedEmojis); const mergedEmojis = [Emojis.categoryFrequentlyUsed, ...formattedFrequentlyUsedEmojis, ...emojis]; + console.log(">>>>", mergedEmojis); return addSpacesToEmojiCategories(mergedEmojis); } From b5ee35c7b983de9787f25b26ace873317ce88e96 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 3 Jun 2024 15:46:52 -0700 Subject: [PATCH 007/192] style --- src/libs/EmojiUtils.ts | 13 ++++++------- .../ComposerWithSuggestions.tsx | 12 +----------- 2 files changed, 7 insertions(+), 18 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index d1ae12d4b0f8..c7d819eb6435 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -47,7 +47,6 @@ Onyx.connect({ }, }); - const getEmojiName = (emoji: Emoji, lang: Locale = CONST.LOCALES.DEFAULT): string => { if (!emoji) { return ''; @@ -218,27 +217,27 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL return addSpacesToEmojiCategories(emojis); } - console.log(">>>> nvp", frequentlyUsedEmojis); + console.log('>>>> nvp', frequentlyUsedEmojis); const formattedFrequentlyUsedEmojis = frequentlyUsedEmojis.map((frequentlyUsedEmoji: Emoji): Emoji => { - console.log(">>>> processing", frequentlyUsedEmoji); + console.log('>>>> processing', frequentlyUsedEmoji); // Frequently used emojis in the old format will have name/types/code stored with them // The back-end may not always have both, so we'll need to fill them in. if (!('code' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { - console.log(">>>> code does not exist"); + console.log('>>>> code does not exist'); return findEmojiByName(frequentlyUsedEmoji.name); } if (!('name' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { - console.log(">>>> name does not exist"); + console.log('>>>> name does not exist'); return findEmojiByCode(frequentlyUsedEmoji.code); } return frequentlyUsedEmoji; }); - console.log(">>>> formatted", formattedFrequentlyUsedEmojis); + console.log('>>>> formatted', formattedFrequentlyUsedEmojis); const mergedEmojis = [Emojis.categoryFrequentlyUsed, ...formattedFrequentlyUsedEmojis, ...emojis]; - console.log(">>>>", mergedEmojis); + console.log('>>>>', mergedEmojis); return addSpacesToEmojiCategories(mergedEmojis); } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 725194ffa455..78e8d79cd325 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -449,17 +449,7 @@ function ComposerWithSuggestions( debouncedBroadcastUserIsTyping(reportID); } }, - [ - findNewlyAddedChars, - preferredLocale, - preferredSkinTone, - reportID, - setIsCommentEmpty, - suggestionsRef, - raiseIsScrollLikelyLayoutTriggered, - debouncedSaveReportComment, - selection.end, - ], + [findNewlyAddedChars, preferredLocale, preferredSkinTone, reportID, setIsCommentEmpty, suggestionsRef, raiseIsScrollLikelyLayoutTriggered, debouncedSaveReportComment, selection.end], ); const prepareCommentAndResetComposer = useCallback((): string => { From 77811017dfff55592e33bd44142d09aa9fe0aff9 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 3 Jun 2024 16:43:11 -0700 Subject: [PATCH 008/192] style --- src/libs/EmojiUtils.ts | 7 ------- src/libs/actions/User.ts | 2 +- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 1 - src/pages/home/report/ReportActionItemMessageEdit.tsx | 1 - tests/unit/EmojiTest.ts | 8 -------- 5 files changed, 1 insertion(+), 18 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index c7d819eb6435..09ad6058b087 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -217,27 +217,20 @@ function mergeEmojisWithFrequentlyUsedEmojis(emojis: PickerEmojis): EmojiPickerL return addSpacesToEmojiCategories(emojis); } - console.log('>>>> nvp', frequentlyUsedEmojis); - const formattedFrequentlyUsedEmojis = frequentlyUsedEmojis.map((frequentlyUsedEmoji: Emoji): Emoji => { - console.log('>>>> processing', frequentlyUsedEmoji); // Frequently used emojis in the old format will have name/types/code stored with them // The back-end may not always have both, so we'll need to fill them in. if (!('code' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { - console.log('>>>> code does not exist'); return findEmojiByName(frequentlyUsedEmoji.name); } if (!('name' in (frequentlyUsedEmoji as FrequentlyUsedEmoji))) { - console.log('>>>> name does not exist'); return findEmojiByCode(frequentlyUsedEmoji.code); } return frequentlyUsedEmoji; }); - console.log('>>>> formatted', formattedFrequentlyUsedEmojis); const mergedEmojis = [Emojis.categoryFrequentlyUsed, ...formattedFrequentlyUsedEmojis, ...emojis]; - console.log('>>>>', mergedEmojis); return addSpacesToEmojiCategories(mergedEmojis); } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index d68f14aae5f2..0222af1798f0 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -35,7 +35,7 @@ import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {BlockedFromConcierge, CustomStatusDraft, FrequentlyUsedEmoji, Policy} from '@src/types/onyx'; +import type {BlockedFromConcierge, CustomStatusDraft, Policy} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 78e8d79cd325..9d31e5df0ecd 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -47,7 +47,6 @@ import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Report from '@userActions/Report'; -import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 0c4460e35909..46d89c753df1 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -36,7 +36,6 @@ import * as ComposerActions from '@userActions/Composer'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Report from '@userActions/Report'; -import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index fe069061d7a3..84442db92553 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -1,15 +1,7 @@ -import {getUnixTime} from 'date-fns'; -import Onyx from 'react-native-onyx'; import Emojis, {importEmojiLocale} from '@assets/emojis'; import type {Emoji} from '@assets/emojis/types'; -import * as User from '@libs/actions/User'; import {buildEmojisTrie} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {FrequentlyUsedEmoji} from '@src/types/onyx'; -import * as TestHelper from '../utils/TestHelper'; -import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; describe('EmojiTest', () => { beforeAll(async () => { From d1693ca78f6b3d1fcd28a8d57a065b19c546b19f Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 4 Jun 2024 13:25:53 -0700 Subject: [PATCH 009/192] remove unused --- src/libs/EmojiUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 09ad6058b087..2ac736f4809d 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -1,4 +1,3 @@ -import {getUnixTime} from 'date-fns'; import Str from 'expensify-common/lib/str'; import memoize from 'lodash/memoize'; import Onyx from 'react-native-onyx'; From 9b6b6994df575312e9fd736c560e5c871029aaeb Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 19 Jun 2024 17:11:24 +0700 Subject: [PATCH 010/192] revert singleExecution approach --- src/components/TaskHeaderActionButton.tsx | 15 +- src/pages/home/ReportScreen.tsx | 161 ++++++++++----------- src/pages/settings/Profile/ProfilePage.tsx | 104 +++++++------ 3 files changed, 131 insertions(+), 149 deletions(-) diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index c56bc6d9704a..0c7e603a4aa2 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -1,4 +1,4 @@ -import React, {useContext} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -10,7 +10,6 @@ import * as Task from '@userActions/Task'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import Button from './Button'; -import {MenuItemGroupContext} from './MenuItemGroup'; type TaskHeaderActionButtonOnyxProps = { /** Current user session */ @@ -25,16 +24,6 @@ type TaskHeaderActionButtonProps = TaskHeaderActionButtonOnyxProps & { function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const {singleExecution} = useContext(MenuItemGroupContext) ?? {}; - - const onPressAction = () => { - const onPress = () => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)); - if (!singleExecution) { - onPress(); - return; - } - singleExecution(onPress)(); - }; if (!ReportUtils.canWriteInReport(report)) { return null; @@ -47,7 +36,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1)} medium text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} - onPress={Session.checkIfActionIsAllowed(onPressAction)} + onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))} style={styles.flex1} /> diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index e0f5fed0192e..20d4f1fa175e 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -12,7 +12,6 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import * as Illustrations from '@components/Icon/Illustrations'; -import MenuItemGroup from '@components/MenuItemGroup'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -659,92 +658,90 @@ function ReportScreen({ return ( - - + - - - {headerView} - {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( - - - - - + {headerView} + {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( + + + + - )} - - {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( - - )} - - - {shouldShowReportActionList && ( - - )} - - {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. - If we prevent rendering the report while they are loading then - we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {shouldShowSkeleton && } - - {isCurrentReportLoadedFromOnyx ? ( - setIsComposerFocus(true)} - onComposerBlur={() => setIsComposerFocus(false)} - report={report} - reportMetadata={reportMetadata} - reportNameValuePairs={reportNameValuePairs} - policy={policy} - pendingAction={reportPendingAction} - isComposerFullSize={!!isComposerFullSize} - isEmptyChat={isEmptyChat} - lastReportAction={lastReportAction} - /> - ) : null} - - - - - + )} + + {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + + )} + + + {shouldShowReportActionList && ( + + )} + + {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. + If we prevent rendering the report while they are loading then + we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} + {shouldShowSkeleton && } + + {isCurrentReportLoadedFromOnyx ? ( + setIsComposerFocus(true)} + onComposerBlur={() => setIsComposerFocus(false)} + report={report} + reportMetadata={reportMetadata} + reportNameValuePairs={reportNameValuePairs} + policy={policy} + pendingAction={reportPendingAction} + isComposerFullSize={!!isComposerFullSize} + isEmptyChat={isEmptyChat} + lastReportAction={lastReportAction} + /> + ) : null} + + + + + ); diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index f33a86ed2d46..011d5956be58 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -1,11 +1,10 @@ -import React, {useContext, useEffect} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; -import MenuItemGroup, {MenuItemGroupContext} from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -61,7 +60,6 @@ function ProfilePage({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const {waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const getPronouns = (): string => { const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; @@ -137,57 +135,55 @@ function ProfilePage({ icon={Illustrations.Profile} /> - - -
- {publicOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)} - brickRoadIndicator={detail.brickRoadIndicator} - /> - ))} -
-
- {isLoadingApp ? ( - - ) : ( - <> - {privateOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)) : () => Navigation.navigate(detail.pageRoute)} - /> - ))} - - )} -
-
-
+ +
+ {publicOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + brickRoadIndicator={detail.brickRoadIndicator} + /> + ))} +
+
+ {isLoadingApp ? ( + + ) : ( + <> + {privateOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + /> + ))} + + )} +
+
); From f0ba486330d804111b5c8a84e583ee9bdb1a1066 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Thu, 20 Jun 2024 10:48:08 +0200 Subject: [PATCH 011/192] Empty State Component for Search --- src/components/AccountingListSkeletonView.tsx | 4 +- src/components/EmptyStateComponent/index.tsx | 100 ++++++++++++++++++ src/components/EmptyStateComponent/types.ts | 29 +++++ src/components/Icon/index.tsx | 2 +- src/components/OptionsListSkeletonView.tsx | 4 +- src/components/Search.tsx | 21 +++- .../Skeletons/ItemListSkeletonView.tsx | 42 +++++--- ...ItemSkeleton.tsx => SearchRowSkeleton.tsx} | 14 ++- src/components/Skeletons/TableRowSkeleton.tsx | 52 +++++++++ src/styles/index.ts | 23 ++++ 10 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 src/components/EmptyStateComponent/index.tsx create mode 100644 src/components/EmptyStateComponent/types.ts rename src/components/Skeletons/{TableListItemSkeleton.tsx => SearchRowSkeleton.tsx} (77%) create mode 100644 src/components/Skeletons/TableRowSkeleton.tsx diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx index b977903d3adc..d04e197edbab 100644 --- a/src/components/AccountingListSkeletonView.tsx +++ b/src/components/AccountingListSkeletonView.tsx @@ -4,12 +4,14 @@ import ItemListSkeletonView from './Skeletons/ItemListSkeletonView'; type AccountingListSkeletonViewProps = { shouldAnimate?: boolean; + gradientOpacity?: boolean; }; -function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) { +function AccountingListSkeletonView({shouldAnimate = true, gradientOpacity = false}: AccountingListSkeletonViewProps) { return ( ( <> { + if (!event) { + return; + } + + if ('naturalSize' in event) { + setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height); + } else { + setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); + } + }; + + let HeaderComponent; + switch (headerMediaType) { + case 'video': + HeaderComponent = ( + + ); + break; + case 'animation': + + 123 + ; + break; + default: + HeaderComponent = ( + + ); + } + + return ( + + + + + + + {HeaderComponent} + + {titleText} + {subtitleText} + + + + + + ); +} + +export default EmptyStateComponent; diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts new file mode 100644 index 000000000000..7a7cc9b4ff57 --- /dev/null +++ b/src/components/EmptyStateComponent/types.ts @@ -0,0 +1,29 @@ +import type DotLottieAnimation from '@components/LottieAnimations/types'; +import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; +import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton; +type MediaTypes = 'video' | 'illustration' | 'animation'; + +type SharedProps = { + SkeletonComponent: ValidSkeletons; + titleText: string; + subtitleText: string; + buttonText?: string; + buttonAction?: () => void; + headerMediaType: T; +}; + +type MediaType = SharedProps & { + headerMedia: HeaderMedia; +}; + +type VideoProps = MediaType; +type IllustrationProps = MediaType; +type AnimationProps = MediaType; + +type EmptyStateComponentProps = VideoProps | IllustrationProps | AnimationProps; + +// eslint-disable-next-line import/prefer-default-export +export type {EmptyStateComponentProps}; diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index b4da5c0b0fa2..f49f74a6724a 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,4 +1,4 @@ -import type {ImageContentFit} from 'expo-image'; +import type {ImageContentFit, ImageStyle} from 'expo-image'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx index 1f09876b18d3..8b2a53b5c59e 100644 --- a/src/components/OptionsListSkeletonView.tsx +++ b/src/components/OptionsListSkeletonView.tsx @@ -17,12 +17,14 @@ function getLinedWidth(index: number): string { type OptionsListSkeletonViewProps = { shouldAnimate?: boolean; + gradientOpacity?: boolean; }; -function OptionsListSkeletonView({shouldAnimate = true}: OptionsListSkeletonViewProps) { +function OptionsListSkeletonView({shouldAnimate = true, gradientOpacity = false}: OptionsListSkeletonViewProps) { return ( { const lineWidth = getLinedWidth(itemIndex); diff --git a/src/components/Search.tsx b/src/components/Search.tsx index e4faa15bfb94..442bc5937b76 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -14,7 +14,6 @@ import * as SearchUtils from '@libs/SearchUtils'; import type {SearchColumnType, SortOrder} from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; -import EmptySearchView from '@pages/Search/EmptySearchView'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -23,10 +22,12 @@ import type {SearchQuery} from '@src/types/onyx/SearchResults'; import type SearchResults from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import EmptyStateComponent from './EmptyStateComponent'; import SelectionList from './SelectionList'; import SearchTableHeader from './SelectionList/SearchTableHeader'; import type {ReportListItemType, TransactionListItemType} from './SelectionList/types'; -import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; +import SearchRowSkeleton from './Skeletons/SearchRowSkeleton'; +import TableRowSkeleton from './Skeletons/TableRowSkeleton'; type SearchProps = { query: SearchQuery; @@ -97,11 +98,21 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const shouldShowEmptyState = !isLoadingItems && isEmptyObject(searchResults?.data); if (isLoadingItems) { - return ; + return ; } if (shouldShowEmptyState) { - return ; + return ( + Navigation.navigate(ROUTES.CONCIERGE)} + buttonText="Go to Workspaces" + /> + ); } const openReport = (item: TransactionListItemType | ReportListItemType) => { @@ -188,7 +199,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { onEndReached={fetchMoreResults} listFooterContent={ isLoadingMoreItems ? ( - diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx index 5c46dbdddbfc..62e1e5afad13 100644 --- a/src/components/Skeletons/ItemListSkeletonView.tsx +++ b/src/components/Skeletons/ItemListSkeletonView.tsx @@ -1,5 +1,6 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -9,9 +10,19 @@ type ListItemSkeletonProps = { shouldAnimate?: boolean; renderSkeletonItem: (args: {itemIndex: number}) => React.ReactNode; fixedNumItems?: number; + gradientOpacity?: boolean; + itemViewStyle?: StyleProp; + itemViewHeight?: number; }; -function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNumItems}: ListItemSkeletonProps) { +function ItemListSkeletonView({ + shouldAnimate = true, + renderSkeletonItem, + fixedNumItems, + gradientOpacity = false, + itemViewStyle = {}, + itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT, +}: ListItemSkeletonProps) { const theme = useTheme(); const themeStyles = useThemeStyles(); @@ -19,22 +30,25 @@ function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNu const skeletonViewItems = useMemo(() => { const items = []; for (let i = 0; i < numItems; i++) { + const opacity = gradientOpacity ? 1 - i / numItems : 1; items.push( - - {renderSkeletonItem({itemIndex: i})} - , + + + + {renderSkeletonItem({itemIndex: i})} + + + , ); } return items; - }, [numItems, shouldAnimate, theme, themeStyles, renderSkeletonItem]); - + }, [numItems, shouldAnimate, theme, themeStyles, renderSkeletonItem, gradientOpacity, itemViewHeight, itemViewStyle]); return ( ( <> ( + <> + + + + + )} + /> + ); +} + +TableListItemSkeleton.displayName = 'TableListItemSkeleton'; + +export default TableListItemSkeleton; diff --git a/src/styles/index.ts b/src/styles/index.ts index b031e665594f..a12090ff275f 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1663,6 +1663,7 @@ const styles = (theme: ThemeColors) => welcomeVideoNarrowLayout: { width: variables.onboardingModalWidth, + height: 500, }, onlyEmojisText: { @@ -5033,6 +5034,28 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeNormal, fontWeight: FontUtils.fontWeight.bold, }, + + skeletonBackground: { + flex: 1, + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + }, + + emptyStateForeground: (isSmallScreenWidth: boolean) => ({ + justifyContent: 'center', + alignItems: 'center', + height: '100%', + padding: isSmallScreenWidth ? 24 : 0, + }), + + emptyStateContent: (isSmallScreenWidth: boolean) => ({ + width: isSmallScreenWidth ? '100%' : 400, + backgroundColor: theme.cardBG, + borderRadius: variables.componentBorderRadiusLarge, + }), } satisfies Styles); type ThemeStyles = ReturnType; From 6d91057bf954ea8979b7da97de69a35382b3526c Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 20 Jun 2024 17:08:26 +0700 Subject: [PATCH 012/192] check active route approach --- src/components/ReportActionItem/TaskView.tsx | 6 ++++++ src/components/TaskHeaderActionButton.tsx | 13 ++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 43e896fe6578..ebe442dd8d75 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -96,6 +96,12 @@ function TaskView({report, ...props}: TaskViewProps) { { + if ( + Navigation.isActiveRoute(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID)) || + Navigation.isActiveRoute(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID)) + ) { + return; + } if (isCompleted) { Task.reopenTask(report); } else { diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 0c7e603a4aa2..ccf3e22ecc5f 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -4,10 +4,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import Button from './Button'; @@ -36,7 +38,16 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1)} medium text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} - onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))} + onPress={Session.checkIfActionIsAllowed(() => { + if (Navigation.isActiveRoute(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID)) || Navigation.isActiveRoute(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID))) { + return; + } + if (ReportUtils.isCompletedTaskReport(report)) { + Task.reopenTask(report); + } else { + Task.completeTask(report); + } + })} style={styles.flex1} /> From 0308f12e27abcd6e6486ebf1d37ebec5a79bcd16 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 20 Jun 2024 17:19:06 +0700 Subject: [PATCH 013/192] remove redundant changes --- src/components/MenuItem.tsx | 10 +- src/pages/home/ReportScreen.tsx | 4 +- src/pages/settings/Profile/ProfilePage.tsx | 101 +++++++++++---------- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index fad11c8e3a79..c1fe4270d4e1 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -394,7 +394,7 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [styles.popoverMenuItem, style]; const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {isExecuting, singleExecution} = useContext(MenuItemGroupContext) ?? {}; + const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; @@ -469,11 +469,15 @@ function MenuItem( } if (onPress && event) { - if (!singleExecution) { + if (!singleExecution || !waitForNavigate) { onPress(event); return; } - singleExecution(onPress)(event); + singleExecution( + waitForNavigate(() => { + onPress(event); + }), + )(); } }; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 20d4f1fa175e..ade50c0e2c9b 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -719,8 +719,8 @@ function ReportScreen({ )} {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. - If we prevent rendering the report while they are loading then - we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} + If we prevent rendering the report while they are loading then + we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} {shouldShowSkeleton && } {isCurrentReportLoadedFromOnyx ? ( diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 011d5956be58..4c5ed88e6898 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; +import MenuItemGroup from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -135,55 +136,57 @@ function ProfilePage({ icon={Illustrations.Profile} /> - -
- {publicOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)} - brickRoadIndicator={detail.brickRoadIndicator} - /> - ))} -
-
- {isLoadingApp ? ( - - ) : ( - <> - {privateOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)} - /> - ))} - - )} -
-
+ + +
+ {publicOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + brickRoadIndicator={detail.brickRoadIndicator} + /> + ))} +
+
+ {isLoadingApp ? ( + + ) : ( + <> + {privateOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + /> + ))} + + )} +
+
+
); From a245b253a4b3b0dec9c22046ab6aa764f5059797 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 7 May 2024 10:34:13 +0200 Subject: [PATCH 014/192] Add handling of actions to TransactionListItem --- .../Search/TransactionListItemRow.tsx | 65 ++++++++++++++++--- src/libs/SearchUtils.ts | 29 ++++++++- src/libs/actions/Search.ts | 23 ++++++- src/types/onyx/SearchResults.ts | 5 +- 4 files changed, 109 insertions(+), 13 deletions(-) diff --git a/src/components/SelectionList/Search/TransactionListItemRow.tsx b/src/components/SelectionList/Search/TransactionListItemRow.tsx index c0fff452d1e5..0a51adf0e6df 100644 --- a/src/components/SelectionList/Search/TransactionListItemRow.tsx +++ b/src/components/SelectionList/Search/TransactionListItemRow.tsx @@ -1,6 +1,7 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import Badge from '@components/Badge'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -14,11 +15,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; +import * as SearchUtils from '@libs/SearchUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {SearchTransactionType} from '@src/types/onyx/SearchResults'; +import type {TranslationPaths} from '@src/languages/types'; +import type {SearchTransactionAction, SearchTransactionType} from '@src/types/onyx/SearchResults'; import ExpenseItemHeaderNarrow from './ExpenseItemHeaderNarrow'; import TextWithIconCell from './TextWithIconCell'; import UserInfoCell from './UserInfoCell'; @@ -35,8 +38,8 @@ type TransactionCellProps = { } & CellProps; type ActionCellProps = { - onButtonPress: () => void; -} & CellProps; + goToItem: () => void; +} & TransactionCellProps; type TotalCellProps = { isChildListItem: boolean; @@ -64,6 +67,18 @@ const getTypeIcon = (type?: SearchTransactionType) => { } }; +const actionTranslationsMap: Record = { + view: 'common.view', + // Todo add translation for Review + review: 'common.view', + done: 'common.done', + paid: 'iou.settledExpensify', + approve: 'iou.approve', + pay: 'iou.pay', + submit: 'common.submit', + hold: 'iou.hold', +}; + function ReceiptCell({transactionItem}: TransactionCellProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -148,17 +163,50 @@ function TypeCell({transactionItem, isLargeScreenWidth}: TransactionCellProps) { ); } -function ActionCell({onButtonPress}: ActionCellProps) { +function ActionCell({transactionItem, goToItem}: ActionCellProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {action, amount} = transactionItem; + + const text = translate(actionTranslationsMap[action]); + + if (['done', 'paid'].includes(action)) { + return ( + + ); + } + + if (['view', 'review'].includes(action)) { + return ( + + {buttonText && buttonAction && ( + + )}
diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts index 7a7cc9b4ff57..a3ef4ad75f59 100644 --- a/src/components/EmptyStateComponent/types.ts +++ b/src/components/EmptyStateComponent/types.ts @@ -1,3 +1,4 @@ +import type {StyleProp, ViewStyle} from 'react-native'; import type DotLottieAnimation from '@components/LottieAnimations/types'; import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton'; @@ -12,6 +13,7 @@ type SharedProps = { subtitleText: string; buttonText?: string; buttonAction?: () => void; + headerStyles?: StyleProp; headerMediaType: T; }; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index c3e50cff3178..3ac4e346ccb5 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -118,6 +118,7 @@ import Meter from '@assets/images/meter.svg'; import MoneyBag from '@assets/images/money-bag.svg'; import MoneyCircle from '@assets/images/money-circle.svg'; import MoneySearch from '@assets/images/money-search.svg'; +import MoneyStack from '@assets/images/money-stack.svg'; import MoneyWaving from '@assets/images/money-waving.svg'; import Monitor from '@assets/images/monitor.svg'; import Mute from '@assets/images/mute.svg'; @@ -366,4 +367,5 @@ export { Clear, CheckCircle, CheckmarkCircle, + MoneyStack, }; diff --git a/src/components/ImageSVG/index.tsx b/src/components/ImageSVG/index.tsx index 3ce04a1a190a..cf58aa873584 100644 --- a/src/components/ImageSVG/index.tsx +++ b/src/components/ImageSVG/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type {SvgProps} from 'react-native-svg'; import type ImageSVGProps from './types'; -function ImageSVG({src, width = '100%', height = '100%', fill, hovered = false, pressed = false, style, pointerEvents, preserveAspectRatio}: ImageSVGProps) { +function ImageSVG({src, width, height = '100%', fill, hovered = false, pressed = false, style, pointerEvents, preserveAspectRatio}: ImageSVGProps) { const ImageSvgComponent = src as React.FC; const additionalProps: Pick = {}; diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 442bc5937b76..449dfb4dfe9f 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -3,6 +3,7 @@ import type {StackNavigationProp} from '@react-navigation/stack'; import React, {useCallback, useEffect, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -23,11 +24,11 @@ import type SearchResults from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import EmptyStateComponent from './EmptyStateComponent'; +import LottieAnimations from './LottieAnimations'; import SelectionList from './SelectionList'; import SearchTableHeader from './SelectionList/SearchTableHeader'; import type {ReportListItemType, TransactionListItemType} from './SelectionList/types'; import SearchRowSkeleton from './Skeletons/SearchRowSkeleton'; -import TableRowSkeleton from './Skeletons/TableRowSkeleton'; type SearchProps = { query: SearchQuery; @@ -50,6 +51,7 @@ function isTransactionListItemType(item: TransactionListItemType | ReportListIte function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); + const {translate} = useLocalize(); const {isLargeScreenWidth} = useWindowDimensions(); const navigation = useNavigation>(); const lastSearchResultsRef = useRef>(); @@ -105,12 +107,11 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { return ( Navigation.navigate(ROUTES.CONCIERGE)} - buttonText="Go to Workspaces" + headerMediaType="animation" + headerMedia={LottieAnimations.Coin} + headerStyles={styles.activeComponentBG} + titleText={translate('search.searchResults.emptyState.title')} + subtitleText={translate('search.searchResults.emptyState.subtitle')} /> ); } diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx index 62e1e5afad13..79b8dec1c183 100644 --- a/src/components/Skeletons/ItemListSkeletonView.tsx +++ b/src/components/Skeletons/ItemListSkeletonView.tsx @@ -1,6 +1,6 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; +import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +15,14 @@ type ListItemSkeletonProps = { itemViewHeight?: number; }; +const getVerticalMargin = (style: StyleProp): number => { + if (!style) { + return 0; + } + const flattenStyle = style instanceof Array ? Object.assign({}, ...style) : style; + return Number((flattenStyle.marginVertical || 0) + (flattenStyle.marginTop || 0) + (flattenStyle.marginBottom || 0)); +}; + function ItemListSkeletonView({ shouldAnimate = true, renderSkeletonItem, @@ -27,13 +35,36 @@ function ItemListSkeletonView({ const themeStyles = useThemeStyles(); const [numItems, setNumItems] = useState(fixedNumItems ?? 0); + + const totalItemHeight = itemViewHeight + getVerticalMargin(itemViewStyle); + + const handleLayout = useCallback( + (event: LayoutChangeEvent) => { + if (fixedNumItems) { + return; + } + + const totalHeight = event.nativeEvent.layout.height; + + const newNumItems = Math.ceil(totalHeight / totalItemHeight); + + if (newNumItems !== numItems) { + setNumItems(newNumItems); + } + }, + [fixedNumItems, numItems, totalItemHeight], + ); + const skeletonViewItems = useMemo(() => { const items = []; for (let i = 0; i < numItems; i++) { const opacity = gradientOpacity ? 1 - i / numItems : 1; items.push( - - + + { - if (fixedNumItems) { - return; - } - - const newNumItems = Math.ceil(event.nativeEvent.layout.height / itemViewHeight); - if (newNumItems === numItems) { - return; - } - setNumItems(newNumItems); - }} + onLayout={handleLayout} > {skeletonViewItems} diff --git a/src/languages/en.ts b/src/languages/en.ts index 3a569801bb6a..3f0ca3299185 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2804,6 +2804,10 @@ export default { title: 'Nothing to show', subtitle: 'Try creating something using the green + button.', }, + emptyState: { + title: 'No expenses to display', + subtitle: 'Try creating something using the green + button.', + }, }, groupedExpenses: 'grouped expenses', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index a2118d55e43c..ad4c3973a6fd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2843,6 +2843,10 @@ export default { title: 'No hay nada que ver aquí', subtitle: 'Por favor intenta crear algo usando el botón verde.', }, + emptyState: { + title: 'Sin gastos de exposición', + subtitle: 'Intenta crear algo utilizando el botón verde.', + }, }, groupedExpenses: 'gastos agrupados', }, diff --git a/src/styles/index.ts b/src/styles/index.ts index a12090ff275f..5bf96be3d3c1 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5056,6 +5056,11 @@ const styles = (theme: ThemeColors) => backgroundColor: theme.cardBG, borderRadius: variables.componentBorderRadiusLarge, }), + + emptyStateHeader: { + borderTopLeftRadius: variables.componentBorderRadiusLarge, + borderTopRightRadius: variables.componentBorderRadiusLarge, + }, } satisfies Styles); type ThemeStyles = ReturnType; From 7fe8f3e78f66078d253a107d4ec9b5a7ed5250a9 Mon Sep 17 00:00:00 2001 From: Taras Perun Date: Mon, 24 Jun 2024 15:47:07 +0200 Subject: [PATCH 021/192] preloaded linking --- src/libs/E2E/reactNativeLaunchingTest.ts | 1 + .../E2E/tests/preloadedLinkingTest.e2e.ts | 77 +++++++++++++++++++ tests/e2e/config.ts | 10 +++ 3 files changed, 88 insertions(+) create mode 100644 src/libs/E2E/tests/preloadedLinkingTest.e2e.ts diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index f23508987268..26b250ce3814 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -39,6 +39,7 @@ const tests: Tests = { [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, + [E2EConfig.TEST_NAMES.PreloadedLinking]: require('./tests/preloadedLinkingTest.e2e').default, }; // Once we receive the TII measurement we know that the app is initialized and ready to be used: diff --git a/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts b/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts new file mode 100644 index 000000000000..e718ef0ecdfc --- /dev/null +++ b/src/libs/E2E/tests/preloadedLinkingTest.e2e.ts @@ -0,0 +1,77 @@ +import {DeviceEventEmitter} from 'react-native'; +import type {NativeConfig} from 'react-native-config'; +import Config from 'react-native-config'; +import Timing from '@libs/actions/Timing'; +import E2ELogin from '@libs/E2E/actions/e2eLogin'; +import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; +import E2EClient from '@libs/E2E/client'; +import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; +import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; +import Navigation from '@libs/Navigation/Navigation'; +import Performance from '@libs/Performance'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; + +const test = (config: NativeConfig) => { + console.debug('[E2E] Logging in for comment linking'); + + const reportID = getConfigValueOrThrow('reportID', config); + const linkedReportActionID = getConfigValueOrThrow('linkedReportActionID', config); + + E2ELogin().then((neededLogin) => { + if (neededLogin) { + return waitForAppLoaded().then(() => E2EClient.submitTestDone()); + } + + const [appearMessagePromise, appearMessageResolve] = getPromiseWithResolve(); + const [switchReportPromise, switchReportResolve] = getPromiseWithResolve(); + + Promise.all([appearMessagePromise, switchReportPromise]) + .then(() => { + console.debug('[E2E] Test completed successfully, exiting…'); + E2EClient.submitTestDone(); + }) + .catch((err) => { + console.debug('[E2E] Error while submitting test results:', err); + }); + + const subscription = DeviceEventEmitter.addListener('onViewableItemsChanged', (res) => { + console.debug('[E2E] Viewable items retrieved, verifying correct message…', res); + if (!!res && res[0]?.item?.reportActionID === linkedReportActionID) { + appearMessageResolve(); + subscription.remove(); + } else { + console.debug(`[E2E] Provided message id '${res?.[0]?.item?.reportActionID}' doesn't match to an expected '${linkedReportActionID}'. Waiting for a next one…`); + } + }); + + Performance.subscribeToMeasurements((entry) => { + if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { + console.debug('[E2E] Sidebar loaded, navigating to a report…'); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + return; + } + + if (entry.name === CONST.TIMING.REPORT_INITIAL_RENDER) { + console.debug('[E2E] Navigating to linked report action…'); + Timing.start(CONST.TIMING.SWITCH_REPORT); + Performance.markStart(CONST.TIMING.SWITCH_REPORT); + + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, linkedReportActionID)); + return; + } + + if (entry.name === CONST.TIMING.CHAT_RENDER) { + E2EClient.submitTestResults({ + branch: Config.E2E_BRANCH, + name: 'Comment linking', + duration: entry.duration, + }); + + switchReportResolve(); + } + }); + }); +}; + +export default test; diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index 6eb6bb839ae2..5bd72913c87c 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -8,6 +8,7 @@ const TEST_NAMES = { ReportTyping: 'Report typing', ChatOpening: 'Chat opening', Linking: 'Linking', + PreloadedLinking: 'Preloaded linking', }; /** @@ -96,6 +97,15 @@ export default { linkedReportID: '5421294415618529', linkedReportActionID: '2845024374735019929', }, + [TEST_NAMES.PreloadedLinking]: { + name: TEST_NAMES.PreloadedLinking, + reportScreen: { + autoFocus: true, + }, + // Crowded Policy (Do Not Delete) Report, has a input bar available: + reportID: '5421294415618529', + linkedReportActionID: '8984197495983183608', // Message 4897 + }, }, }; From 9c096648ad8ab8e0572cba385c74114a90740f00 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 25 Jun 2024 08:25:13 +0200 Subject: [PATCH 022/192] Small fixes on search empty page, add new empty component on tags and category pages --- src/components/EmptyStateComponent/index.tsx | 6 +++--- src/components/EmptyStateComponent/types.ts | 4 ++-- src/components/Search.tsx | 16 ++-------------- .../Skeletons/ItemListSkeletonView.tsx | 2 +- src/components/Skeletons/SearchRowSkeleton.tsx | 1 + src/languages/en.ts | 4 ---- src/languages/es.ts | 4 ---- src/pages/Search/EmptySearchView.tsx | 14 ++++++++++---- .../categories/WorkspaceCategoriesPage.tsx | 11 ++++++++--- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 11 ++++++++--- 10 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index a6bd07b94550..24f75fa832f5 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -20,7 +20,7 @@ type VideoLoadedEventType = { }; }; -function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, titleText, subtitleText, headerStyles}: EmptyStateComponentProps) { +function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, title, subtitle, headerStyles}: EmptyStateComponentProps) { const styles = useThemeStyles(); const isSmallScreenWidth = getIsSmallScreenWidth(); @@ -85,8 +85,8 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b {HeaderComponent} - {titleText} - {subtitleText} + {title} + {subtitle} {buttonText && buttonAction && (