From 9f57e200b87294a39867472146753acea9b77f2f Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:10:10 +0530 Subject: [PATCH 001/352] Add tests for group chat name --- __mocks__/@react-native-reanimated/index.ts | 10 + src/pages/InviteReportParticipantsPage.tsx | 17 +- tests/ui/GroupChatNameTests.tsx | 393 ++++++++++++++++++++ tests/ui/UnreadIndicatorsTest.tsx | 8 +- tests/unit/ReportUtilsTest.ts | 94 +++++ tests/utils/LHNTestUtils.tsx | 20 +- tests/utils/TestHelper.ts | 59 +++ 7 files changed, 584 insertions(+), 17 deletions(-) create mode 100644 __mocks__/@react-native-reanimated/index.ts create mode 100644 tests/ui/GroupChatNameTests.tsx diff --git a/__mocks__/@react-native-reanimated/index.ts b/__mocks__/@react-native-reanimated/index.ts new file mode 100644 index 000000000000..28efba1dde69 --- /dev/null +++ b/__mocks__/@react-native-reanimated/index.ts @@ -0,0 +1,10 @@ +// __mocks__/react-native-reanimated/index.js +const actualAnimated = jest.requireActual('react-native-reanimated/mock'); + +const mock = { + ...actualAnimated, + createAnimatedPropAdapter: jest.fn(), + useReducedMotion: jest.fn(), +}; + +export default mock; diff --git a/src/pages/InviteReportParticipantsPage.tsx b/src/pages/InviteReportParticipantsPage.tsx index a4d5c5518ba2..4db57f5f2f01 100644 --- a/src/pages/InviteReportParticipantsPage.tsx +++ b/src/pages/InviteReportParticipantsPage.tsx @@ -11,6 +11,7 @@ import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem import type {Section} from '@components/SelectionList/types'; import withNavigationTransitionEnd from '@components/withNavigationTransitionEnd'; import type {WithNavigationTransitionEndProps} from '@components/withNavigationTransitionEnd'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -44,7 +45,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen const styles = useThemeStyles(); const {translate} = useLocalize(); - const [searchTerm, setSearchTerm] = useState(''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); const [selectedOptions, setSelectedOptions] = useState([]); const [invitePersonalDetails, setInvitePersonalDetails] = useState([]); const [recentReports, setRecentReports] = useState([]); @@ -57,7 +58,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen ); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], searchTerm, excludedUsers, false, options.reports, true); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(options.personalDetails, betas ?? [], debouncedSearchTerm, excludedUsers, false, options.reports, true); // Update selectedOptions with the latest personalDetails information const detailsMap: Record = {}; @@ -77,7 +78,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen setRecentReports(inviteOptions.recentReports); setSelectedOptions(newSelectedOptions); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change - }, [personalDetails, betas, searchTerm, excludedUsers, options]); + }, [personalDetails, betas, debouncedSearchTerm, excludedUsers, options]); const sections = useMemo(() => { const sectionsArr: Sections = []; @@ -88,11 +89,11 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; - if (searchTerm !== '') { + if (debouncedSearchTerm !== '') { filterSelectedOptions = selectedOptions.filter((option) => { const accountID = option?.accountID; const isOptionInPersonalDetails = invitePersonalDetails.some((personalDetail) => accountID && personalDetail?.accountID === accountID); - const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm); const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); return isPartOfSearchTerm || isOptionInPersonalDetails; }); @@ -130,7 +131,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen } return sectionsArr; - }, [invitePersonalDetails, searchTerm, selectedOptions, translate, userToInvite, areOptionsInitialized, recentReports]); + }, [invitePersonalDetails, debouncedSearchTerm, selectedOptions, translate, userToInvite, areOptionsInitialized, recentReports]); const toggleOption = useCallback( (option: OptionsListUtils.MemberForList) => { @@ -171,7 +172,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen }, [selectedOptions, backRoute, reportID, validate]); const headerMessage = useMemo(() => { - const searchValue = searchTerm.trim().toLowerCase(); + const searchValue = debouncedSearchTerm.trim().toLowerCase(); const expensifyEmails = CONST.EXPENSIFY_EMAILS as string[]; if (!userToInvite && expensifyEmails.includes(searchValue)) { return translate('messages.errorMessageInvalidEmail'); @@ -187,7 +188,7 @@ function InviteReportParticipantsPage({betas, personalDetails, report, didScreen return translate('messages.userIsAlreadyMember', {login: searchValue, name: reportName ?? ''}); } return OptionsListUtils.getHeaderMessage(invitePersonalDetails.length !== 0, !!userToInvite, searchValue); - }, [searchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); + }, [debouncedSearchTerm, userToInvite, excludedUsers, invitePersonalDetails, translate, reportName]); const footerContent = useMemo( () => ( diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx new file mode 100644 index 000000000000..e0ac71120768 --- /dev/null +++ b/tests/ui/GroupChatNameTests.tsx @@ -0,0 +1,393 @@ +/* eslint-disable testing-library/no-node-access */ +import type * as NativeNavigation from '@react-navigation/native'; +import {act, render, screen, waitFor} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import * as Localize from '@libs/Localize'; +import * as AppActions from '@userActions/App'; +import * as User from '@userActions/User'; +import App from '@src/App'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Participant} from '@src/types/onyx/Report'; +import PusherHelper from '../utils/PusherHelper'; +import * as TestHelper from '../utils/TestHelper'; +import {navigateToSidebarOption} from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; + +// We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App +jest.setTimeout(50000); + +jest.mock('../../src/components/ConfirmedRoute.tsx'); + +// Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest +jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ + /* eslint-disable-next-line @typescript-eslint/naming-convention */ + __esModule: true, + default: { + ignoreLogs: jest.fn(), + ignoreAllLogs: jest.fn(), + }, +})); + +/** + * We need to keep track of the transitionEnd callback so we can trigger it in our tests + */ +let transitionEndCB: () => void; + +type ListenerMock = { + triggerTransitionEnd: () => void; + addListener: jest.Mock; +}; + +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * This can't be moved to a utils file because Jest wants any external function to stay in the scope. + * Details: https://github.com/jestjs/jest/issues/2567 + */ +const createAddListenerMock = (): ListenerMock => { + const transitionEndListeners: Array<() => void> = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback: () => void) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + const {triggerTransitionEnd, addListener} = createAddListenerMock(); + transitionEndCB = triggerTransitionEnd; + + const useNavigation = () => + ({ + navigate: jest.fn(), + ...actualNav.useNavigation, + getState: () => ({ + routes: [], + }), + addListener, + } as typeof NativeNavigation.useNavigation); + + return { + ...actualNav, + useNavigation, + getState: () => ({ + routes: [], + }), + } as typeof NativeNavigation; +}); + +beforeAll(() => { + TestHelper.beforeAllSetupUITests(); +}); + +const REPORT_ID = '1'; +const USER_A_ACCOUNT_ID = 1; +const USER_A_EMAIL = 'user_a@test.com'; +const USER_B_ACCOUNT_ID = 2; +const USER_B_EMAIL = 'user_b@test.com'; +const USER_C_ACCOUNT_ID = 3; +const USER_C_EMAIL = 'user_c@test.com'; +const USER_D_ACCOUNT_ID = 4; +const USER_D_EMAIL = 'user_d@test.com'; +const USER_E_ACCOUNT_ID = 5; +const USER_E_EMAIL = 'user_e@test.com'; +const USER_F_ACCOUNT_ID = 6; +const USER_F_EMAIL = 'user_f@test.com'; +const USER_G_ACCOUNT_ID = 7; +const USER_G_EMAIL = 'user_g@test.com'; +const USER_H_ACCOUNT_ID = 8; +const USER_H_EMAIL = 'user_h@test.com'; + +/** + * Sets up a test with a logged in user. Returns the test instance. + */ +function signInAndGetApp(reportName = '', participantAccountIDs?: number[]): Promise { + // Render the App and sign in as a test user. + render(); + + const participants: Record = {}; + participantAccountIDs?.forEach((id) => { + participants[id] = { + hidden: false, + role: id === 1 ? CONST.REPORT.ROLE.ADMIN : CONST.REPORT.ROLE.MEMBER, + } as Participant; + }); + + return waitForBatchedUpdatesWithAct() + .then(async () => { + await waitForBatchedUpdatesWithAct(); + const hintText = Localize.translateLocal('loginForm.loginForm'); + const loginForm = screen.queryAllByLabelText(hintText); + expect(loginForm).toHaveLength(1); + + await act(async () => { + await TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }); + return waitForBatchedUpdatesWithAct(); + }) + .then(() => { + User.subscribeToUserEvents(); + return waitForBatchedUpdates(); + }) + .then(async () => { + // Simulate setting an unread report and personal details + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + reportID: REPORT_ID, + reportName, + lastMessageText: 'Test', + participants, + lastActorAccountID: USER_B_ACCOUNT_ID, + type: CONST.REPORT.TYPE.CHAT, + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + }); + + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, { + [USER_A_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_A_EMAIL, USER_A_ACCOUNT_ID, 'A'), + [USER_B_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_B_EMAIL, USER_B_ACCOUNT_ID, 'B'), + [USER_C_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_C_EMAIL, USER_C_ACCOUNT_ID, 'C'), + [USER_D_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_D_EMAIL, USER_D_ACCOUNT_ID, 'D'), + [USER_E_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_E_EMAIL, USER_E_ACCOUNT_ID, 'E'), + [USER_F_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_F_EMAIL, USER_F_ACCOUNT_ID, 'F'), + [USER_G_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_G_EMAIL, USER_G_ACCOUNT_ID, 'G'), + [USER_H_ACCOUNT_ID]: TestHelper.buildPersonalDetails(USER_H_EMAIL, USER_H_ACCOUNT_ID, 'H'), + }); + + // We manually setting the sidebar as loaded since the onLayout event does not fire in tests + AppActions.setSidebarLoaded(); + return waitForBatchedUpdatesWithAct(); + }); +} + +/** + * Tests for checking the group chat names at places like LHN, chat header, details page etc. + * Note that limit of 5 names is only for the header. + */ +describe('Tests for group chat name', () => { + beforeEach(() => { + jest.clearAllMocks(); + Onyx.clear(); + + // Unsubscribe to pusher channels + PusherHelper.teardown(); + }); + + const participantAccountIDs4 = [USER_A_ACCOUNT_ID, USER_B_ACCOUNT_ID, USER_C_ACCOUNT_ID, USER_D_ACCOUNT_ID]; + const participantAccountIDs8 = [...participantAccountIDs4, USER_E_ACCOUNT_ID, USER_F_ACCOUNT_ID, USER_G_ACCOUNT_ID, USER_H_ACCOUNT_ID]; + + it('Should show correctly in LHN', () => + signInAndGetApp('A, B, C, D', participantAccountIDs4).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D')); + })); + + it('Should show correctly in LHN when report name is not present', () => + signInAndGetApp('', participantAccountIDs4).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D')); + })); + + it('Should show all 8 names in LHN when 8 participants are present', () => + signInAndGetApp('', participantAccountIDs8).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E, F, G, H')); + })); + + it('Check if group name shows fine for report header', () => + signInAndGetApp('', participantAccountIDs4) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D'); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = 'A, B, C, D'; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show only 5 names when there are 8 participants in the report header', () => + signInAndGetApp('', participantAccountIDs8) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E, F, G, H'); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = 'A, B, C, D, E'; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show exact name in header when report name is available with 4 participants', () => + signInAndGetApp('Test chat', participantAccountIDs4) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe('Test chat'); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = 'Test chat'; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show exact name in header when report name is available with 8 participants', () => + signInAndGetApp("Let's talk", participantAccountIDs8) + .then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + expect(displayNameText?.props?.children?.[0]).toBe("Let's talk"); + + return navigateToSidebarOption(0); + }) + .then(waitForBatchedUpdates) + .then(async () => { + await act(() => transitionEndCB?.()); + const name = "Let's talk"; + const displayNameTexts = screen.queryAllByLabelText(name); + return waitFor(() => expect(displayNameTexts).toHaveLength(1)); + })); + + it('Should show last message preview in LHN', () => + signInAndGetApp('A, B, C, D', participantAccountIDs4).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const lastChatHintText = Localize.translateLocal('accessibilityHints.lastChatMessagePreview'); + const lastChatText = screen.queryByLabelText(lastChatHintText); + + return waitFor(() => expect(lastChatText?.props?.children).toBe('B: Test')); + })); + + it('Should sort the names before displaying', () => + signInAndGetApp('', [USER_E_ACCOUNT_ID, ...participantAccountIDs4]).then(() => { + // Verify the sidebar links are rendered + const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); + const sidebarLinks = screen.queryAllByLabelText(sidebarLinksHintText); + expect(sidebarLinks).toHaveLength(1); + + // Verify there is only one option in the sidebar + const optionRowsHintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(optionRowsHintText); + expect(optionRows).toHaveLength(1); + + const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); + const displayNameText = screen.queryByLabelText(displayNameHintText); + + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E')); + })); +}); diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 1d31a707d81d..1a0bd3aeb279 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -25,6 +25,7 @@ import type {ReportAction, ReportActions} from '@src/types/onyx'; import type {NativeNavigationMock} from '../../__mocks__/@react-navigation/native'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; +import {navigateToSidebarOption} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -82,13 +83,6 @@ function navigateToSidebar(): Promise { return waitForBatchedUpdates(); } -async function navigateToSidebarOption(index: number): Promise { - const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); - const optionRows = screen.queryAllByAccessibilityHint(hintText); - fireEvent(optionRows[index], 'press'); - await waitForBatchedUpdatesWithAct(); -} - function areYouOnChatListScreen(): boolean { const hintText = Localize.translateLocal('sidebarScreen.listOfChats'); const sidebarLinks = screen.queryAllByLabelText(hintText); diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 598c0e3bcbd6..29502ba6177e 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -10,6 +10,7 @@ import type {PersonalDetailsList, Policy, Report, ReportAction} from '@src/types import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet'; import * as NumberUtils from '../../src/libs/NumberUtils'; import * as LHNTestUtils from '../utils/LHNTestUtils'; +import {fakePersonalDetails} from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; // Be sure to include the mocked permissions library or else the beta tests won't work @@ -991,4 +992,97 @@ describe('ReportUtils', () => { expect(report).toEqual(undefined); }); }); + + describe('getGroupChatName tests', () => { + afterEach(() => Onyx.clear()); + + describe('When participantAccountIDs is passed to getGroupChatName', () => { + it('Should show all participants name if count <= 5 and shouldApplyLimit is false', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4])).toEqual('Four, One, Three, Two'); + }); + + it('Should show all participants name if count <= 5 and shouldApplyLimit is true', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('Four, One, Three, Two'); + }); + + it('Should show 5 participants name if count > 5 and shouldApplyLimit is true', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], true)).toEqual('Five, Four, One, Three, Two'); + }); + + it('Should show all participants name if count > 5 and shouldApplyLimit is false', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], false)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); + }); + + it('Should use correct display name for participants', async () => { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, participantsPersonalDetails); + expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('(833) 240-3627, floki@vikings.net, Lagertha, Ragnar'); + }); + }); + + describe('When participantAccountIDs is not passed to getGroupChatName and report ID is passed', () => { + it('Should show report name if count <= 5 and shouldApplyLimit is false', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4], 0, false, [1]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, false, report)).toEqual("Let's talk"); + }); + + it('Should show report name if count <= 5 and shouldApplyLimit is true', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4], 0, false, [1]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, true, report)).toEqual("Let's talk"); + }); + + it('Should show report name if count > 5 and shouldApplyLimit is true', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4, 5, 6, 7, 8], 0, false, [1, 2]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, true, report)).toEqual("Let's talk"); + }); + + it('Should show report name if count > 5 and shouldApplyLimit is false', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4, 5, 6, 7, 8], 0, false, [1, 2]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: "Let's talk", + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, false, report)).toEqual("Let's talk"); + }); + + it('Should show participant names if report name is not available', async () => { + const report = { + ...LHNTestUtils.getFakeReport([1, 2, 3, 4, 5, 6, 7, 8], 0, false, [1, 2]), + chatType: CONST.REPORT.CHAT_TYPE.GROUP, + reportID: `1`, + reportName: '', + }; + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}1`, report); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); + expect(ReportUtils.getGroupChatName(undefined, false, report)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); + }); + }); + }); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 7197529cd43c..1d8a13ead7d6 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -112,6 +112,13 @@ const fakePersonalDetails: PersonalDetailsList = { avatar: 'none', firstName: 'Nine', }, + 10: { + accountID: 10, + login: 'email10@test.com', + displayName: 'Email Ten', + avatar: 'none', + firstName: 'Ten', + }, }; let lastFakeReportID = 0; @@ -120,16 +127,25 @@ let lastFakeReportActionID = 0; /** * @param millisecondsInThePast the number of milliseconds in the past for the last message timestamp (to order reports by most recent messages) */ -function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0, isUnread = false): Report { +function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0, isUnread = false, adminIDs: number[] = []): Report { const lastVisibleActionCreated = DateUtils.getDBTime(Date.now() - millisecondsInThePast); + const participants = ReportUtils.buildParticipantsFromAccountIDs(participantAccountIDs); + + adminIDs.forEach((id) => { + participants[id] = { + hidden: false, + role: CONST.REPORT.ROLE.ADMIN, + }; + }); + return { type: CONST.REPORT.TYPE.CHAT, reportID: `${++lastFakeReportID}`, reportName: 'Report', lastVisibleActionCreated, lastReadTime: isUnread ? DateUtils.subtractMillisecondsFromDateTime(lastVisibleActionCreated, 1) : lastVisibleActionCreated, - participants: ReportUtils.buildParticipantsFromAccountIDs(participantAccountIDs), + participants, }; } diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 84ea2b2aafbe..6f97c23a5b29 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,3 +1,4 @@ +import {fireEvent, screen} from '@testing-library/react-native'; import {Str} from 'expensify-common'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; @@ -13,6 +14,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import appSetup from '@src/setup'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; +import waitForBatchedUpdatesWithAct from './waitForBatchedUpdatesWithAct'; +import * as Localize from '@libs/Localize'; type MockFetch = jest.MockedFn & { pause: () => void; @@ -305,6 +308,59 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } +/** + * This is a helper function to create a mock for the addListener function of the react-navigation library. + * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate + * the transitionEnd event that is triggered when the screen transition animation is completed. + * + * @returns An object with two functions: triggerTransitionEnd and addListener + */ +const createAddListenerMock = () => { + const transitionEndListeners: Listener[] = []; + const triggerTransitionEnd = () => { + transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); + }; + + const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { + if (listener === 'transitionEnd') { + transitionEndListeners.push(callback); + } + return () => { + transitionEndListeners.filter((cb) => cb !== callback); + }; + }); + + return {triggerTransitionEnd, addListener}; +}; + +async function navigateToSidebarOption(index: number): Promise { + const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); + const optionRows = screen.queryAllByAccessibilityHint(hintText); + fireEvent(optionRows[index], 'press'); + await waitForBatchedUpdatesWithAct(); +} + +function beforeAllSetupUITests(shouldConnectToPusher = false) { + // In this test, we are generically mocking the responses of all API requests by mocking fetch() and having it + // return 200. In other tests, we might mock HttpUtils.xhr() with a more specific mock data response (which means + // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling + // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to + // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. + global.fetch = getGlobalFetchMock(); + + Linking.setInitialURL('https://new.expensify.com/'); + appSetup(); + + if (shouldConnectToPusher) { + PusherConnectionManager.init(); + Pusher.init({ + appKey: CONFIG.PUSHER.APP_KEY, + cluster: CONFIG.PUSHER.CLUSTER, + authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`, + }); + } +} + export type {MockFetch, FormData}; export { assertFormDataMatchesObject, @@ -318,4 +374,7 @@ export { expectAPICommandToHaveBeenCalled, expectAPICommandToHaveBeenCalledWith, setupGlobalFetchMock, + createAddListenerMock, + navigateToSidebarOption, + beforeAllSetupUITests, }; From 7b5298a9e61ceeefdb87d3398902a6038602bffa Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 2 Aug 2024 02:34:04 +0530 Subject: [PATCH 002/352] Update --- __mocks__/@react-native-reanimated/index.ts | 3 ++- tests/ui/GroupChatNameTests.tsx | 4 ++++ tests/utils/TestHelper.ts | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/__mocks__/@react-native-reanimated/index.ts b/__mocks__/@react-native-reanimated/index.ts index 28efba1dde69..df9cc4ecef8d 100644 --- a/__mocks__/@react-native-reanimated/index.ts +++ b/__mocks__/@react-native-reanimated/index.ts @@ -1,4 +1,5 @@ -// __mocks__/react-native-reanimated/index.js +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + const actualAnimated = jest.requireActual('react-native-reanimated/mock'); const mock = { diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index e0ac71120768..7eb70b412f70 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -1,4 +1,8 @@ /* eslint-disable testing-library/no-node-access */ + +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ import type * as NativeNavigation from '@react-navigation/native'; import {act, render, screen, waitFor} from '@testing-library/react-native'; import React from 'react'; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 6f97c23a5b29..b10f165a96be 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,8 +1,10 @@ import {fireEvent, screen} from '@testing-library/react-native'; import {Str} from 'expensify-common'; +import type {Listener} from 'onfido-sdk-ui/types/shared/EventEmitter'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; +import * as Localize from '@libs/Localize'; import * as Pusher from '@libs/Pusher/pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import CONFIG from '@src/CONFIG'; @@ -15,7 +17,6 @@ import appSetup from '@src/setup'; import type {Response as OnyxResponse, PersonalDetails, Report} from '@src/types/onyx'; import waitForBatchedUpdates from './waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from './waitForBatchedUpdatesWithAct'; -import * as Localize from '@libs/Localize'; type MockFetch = jest.MockedFn & { pause: () => void; @@ -318,6 +319,7 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { const createAddListenerMock = () => { const transitionEndListeners: Listener[] = []; const triggerTransitionEnd = () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); }; From 8d93e580e3629f763c2236d4f32ec06bf0142636 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 5 Aug 2024 13:04:15 +0200 Subject: [PATCH 003/352] WIP on emojis enlarge --- src/CONST.ts | 8 ++- .../HTMLRenderers/EmojiRenderer.tsx | 14 +++- .../InlineCodeBlock/WrappedText.tsx | 2 +- src/libs/EmojiUtils.ts | 64 +++++++++++++++++- src/libs/ValidationUtils.ts | 4 +- .../home/report/ReportActionItemFragment.tsx | 17 ++--- .../index.native.tsx | 67 +++++++++++++++++++ .../index.tsx | 47 +++++++++++++ .../report/comment/TextCommentFragment.tsx | 48 +++++++++---- .../TextWithEmojiFragment/index.native.tsx | 59 ++++++++++++++++ .../comment/TextWithEmojiFragment/index.tsx | 51 ++++++++++++++ src/styles/index.ts | 7 ++ src/styles/variables.ts | 11 +-- 13 files changed, 360 insertions(+), 39 deletions(-) create mode 100644 src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx create mode 100644 src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx create mode 100644 src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx create mode 100644 src/pages/home/report/comment/TextWithEmojiFragment/index.tsx diff --git a/src/CONST.ts b/src/CONST.ts index ed4de999c78c..c59a67a02b41 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2306,8 +2306,8 @@ const CONST = { // eslint-disable-next-line max-len, no-misleading-character-class EMOJI: /[\p{Extended_Pictographic}\u200d\u{1f1e6}-\u{1f1ff}\u{1f3fb}-\u{1f3ff}\u{e0020}-\u{e007f}\u20E3\uFE0F]|[#*0-9]\uFE0F?\u20E3/gu, - // eslint-disable-next-line max-len, no-misleading-character-class - EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/gu, + // eslint-disable-next-line max-len, no-misleading-character-class, no-empty-character-class + EMOJIS: /[\p{Extended_Pictographic}](\u200D[\p{Extended_Pictographic}]|[\u{1F3FB}-\u{1F3FF}]|[\u{E0020}-\u{E007F}]|\uFE0F|\u20E3)*|[\u{1F1E6}-\u{1F1FF}]{2}|[#*0-9]\uFE0F?\u20E3/du, // eslint-disable-next-line max-len, no-misleading-character-class EMOJI_SKIN_TONES: /[\u{1f3fb}-\u{1f3ff}]/gu, @@ -2345,6 +2345,10 @@ const CONST = { return new RegExp(`[\\n\\s]|${this.SPECIAL_CHAR.source}|${this.EMOJI.source}`, 'gu'); }, + get ALL_EMOJIS() { + return new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g')); + }, + MERGED_ACCOUNT_PREFIX: /^(MERGED_\d+@)/, ROUTES: { diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx index 9ad138444b9c..2764f4edbe7e 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -1,11 +1,21 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; import EmojiWithTooltip from '@components/EmojiWithTooltip'; import useThemeStyles from '@hooks/useThemeStyles'; function EmojiRenderer({tnode}: CustomRendererProps) { const styles = useThemeStyles(); - const style = 'islarge' in tnode.attributes ? styles.onlyEmojisText : {}; + const style = useMemo(() => { + if ('islarge' in tnode.attributes) { + return styles.onlyEmojisText; + } + + if ('ismedium' in tnode.attributes) { + return [styles.emojisWithTextFontSize, styles.verticalAlignMiddle]; + } + + return null; + }, [tnode.attributes, styles]); return ( Emojis.emojiNameTable[name]; @@ -148,7 +152,7 @@ function trimEmojiUnicode(emojiCode: string): string { */ function isFirstLetterEmoji(message: string): boolean { const trimmedMessage = Str.replaceAll(message.replace(/ /g, ''), '\n', ''); - const match = trimmedMessage.match(CONST.REGEX.EMOJIS); + const match = trimmedMessage.match(CONST.REGEX.ALL_EMOJIS); if (!match) { return false; @@ -162,7 +166,7 @@ function isFirstLetterEmoji(message: string): boolean { */ function containsOnlyEmojis(message: string): boolean { const trimmedMessage = Str.replaceAll(message.replace(/ /g, ''), '\n', ''); - const match = trimmedMessage.match(CONST.REGEX.EMOJIS); + const match = trimmedMessage.match(CONST.REGEX.ALL_EMOJIS); if (!match) { return false; @@ -285,7 +289,7 @@ function extractEmojis(text: string): Emoji[] { } // Parse Emojis including skin tones - Eg: ['👩🏻', '👩🏻', '👩🏼', '👩🏻', '👩🏼', '👩'] - const parsedEmojis = text.match(CONST.REGEX.EMOJIS); + const parsedEmojis = text.match(CONST.REGEX.ALL_EMOJIS); if (!parsedEmojis) { return []; @@ -586,6 +590,59 @@ function getSpacersIndexes(allEmojis: EmojiPickerList): number[] { return spacersIndexes; } +/** Splits the text with emojis into array if emojis exist in the text */ +function splitTextWithEmojis(text = ''): TextWithEmoji[] { + if (!text) { + return []; + } + + const doesTextContainEmojis = CONST.REGEX.ALL_EMOJIS.test(text); + + if (!doesTextContainEmojis) { + return []; + } + + // The regex needs to be cloned because `exec()` is a stateful operation and maintains the state inside + // the regex variable itself, so we must have an independent instance for each function's call. + const emojisRegex = CONST.REGEX.ALL_EMOJIS; + + const splitText: TextWithEmoji[] = []; + let regexResult: RegExpExecArray | null; + let lastMatchIndexEnd = 0; + + do { + regexResult = emojisRegex.exec(text); + + if (regexResult?.indices) { + const matchIndexStart = regexResult.indices[0][0]; + const matchIndexEnd = regexResult.indices[0][1]; + + if (matchIndexStart > lastMatchIndexEnd) { + splitText.push({ + text: text.slice(lastMatchIndexEnd, matchIndexStart), + isEmoji: false, + }); + } + + splitText.push({ + text: text.slice(matchIndexStart, matchIndexEnd), + isEmoji: true, + }); + + lastMatchIndexEnd = matchIndexEnd; + } + } while (regexResult !== null); + + if (lastMatchIndexEnd < text.length) { + splitText.push({ + text: text.slice(lastMatchIndexEnd, text.length), + isEmoji: false, + }); + } + + return splitText; +} + export type {HeaderIndice, EmojiPickerList, EmojiSpacer, EmojiPickerListItem}; export { @@ -611,4 +668,5 @@ export { hasAccountIDEmojiReacted, getRemovedSkinToneEmoji, getSpacersIndexes, + splitTextWithEmojis, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index b0c99f4a6026..7e2b62fa8281 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -41,7 +41,7 @@ function isValidAddress(value: FormValue): boolean { return false; } - if (!CONST.REGEX.ANY_VALUE.test(value) || value.match(CONST.REGEX.EMOJIS)) { + if (!CONST.REGEX.ANY_VALUE.test(value) || value.match(CONST.REGEX.ALL_EMOJIS)) { return false; } @@ -331,7 +331,7 @@ function isValidRoutingNumber(routingNumber: string): boolean { * Checks that the provided name doesn't contain any emojis */ function isValidCompanyName(name: string) { - return !name.match(CONST.REGEX.EMOJIS); + return !name.match(CONST.REGEX.ALL_EMOJIS); } function isValidReportName(name: string) { diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 787904d72b81..64b88045e385 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -2,12 +2,12 @@ import React, {memo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import RenderHTML from '@components/RenderHTML'; import Text from '@components/Text'; -import UserDetailsTooltip from '@components/UserDetailsTooltip'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; import * as ReportUtils from '@libs/ReportUtils'; +import ReportActionItemMessageHeaderSender from '@pages/home/report/ReportActionItemMessageHeaderSender'; import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {DecisionName, OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; @@ -160,18 +160,13 @@ function ReportActionItemFragment({ } return ( - - - {fragment?.text} - - + fragmentText={fragment.text} + actorIcon={actorIcon} + isSingleLine={isSingleLine} + /> ); } case 'LINK': diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx new file mode 100644 index 000000000000..94da845f4030 --- /dev/null +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx @@ -0,0 +1,67 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; + +type ReportActionItemMessageHeaderSenderProps = { + /** Text to display */ + fragmentText: string; + + /** Users accountID */ + accountID: number; + + /** Should this fragment be contained in a single line? */ + isSingleLine?: boolean; + + /** The accountID of the copilot who took this action on behalf of the user */ + delegateAccountID?: number; + + /** Actor icon */ + actorIcon?: OnyxCommon.Icon; +}; + +function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateAccountID, actorIcon, isSingleLine}: ReportActionItemMessageHeaderSenderProps) { + const styles = useThemeStyles(); + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(fragmentText), [fragmentText]); + + return ( + + {processedTextArray.length !== 0 ? ( + + {processedTextArray.map(({text, isEmoji}) => + isEmoji ? ( + + 😁 + + ) : ( + + {text} + + ), + )} + + ) : ( + + {fragmentText} + + )} + + ); +} + +ReportActionItemMessageHeaderSender.displayName = 'ReportActionItemMessageHeaderSender'; + +export default ReportActionItemMessageHeaderSender; diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx new file mode 100644 index 000000000000..7e4e21296db3 --- /dev/null +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx @@ -0,0 +1,47 @@ +import React, {useMemo} from 'react'; +import Text from '@components/Text'; +import UserDetailsTooltip from '@components/UserDetailsTooltip'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; + +type ReportActionItemMessageHeaderSenderProps = { + /** Text to display */ + fragmentText: string; + + /** Users accountID */ + accountID: number; + + /** Should this fragment be contained in a single line? */ + isSingleLine?: boolean; + + /** The accountID of the copilot who took this action on behalf of the user */ + delegateAccountID?: number; + + /** Actor icon */ + actorIcon?: OnyxCommon.Icon; +}; + +function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateAccountID, actorIcon, isSingleLine}: ReportActionItemMessageHeaderSenderProps) { + const styles = useThemeStyles(); + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(fragmentText), [fragmentText]); + + return ( + + + {processedTextArray.length !== 0 ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) : fragmentText} + + + ); +} + +ReportActionItemMessageHeaderSender.displayName = 'ReportActionItemMessageHeaderSender'; + +export default ReportActionItemMessageHeaderSender; diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 68827de96172..4722271492e7 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -1,6 +1,6 @@ import {Str} from 'expensify-common'; import {isEmpty} from 'lodash'; -import React, {memo} from 'react'; +import React, {memo, useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import ZeroWidthView from '@components/ZeroWidthView'; @@ -17,6 +17,7 @@ import type {OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; import type {Message} from '@src/types/onyx/ReportAction'; import RenderCommentHTML from './RenderCommentHTML'; import shouldRenderAsText from './shouldRenderAsText'; +import TextWithEmojiFragment from './TextWithEmojiFragment'; type TextCommentFragmentProps = { /** The reportAction's source */ @@ -56,7 +57,14 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so const editedTag = fragment?.isEdited ? `` : ''; const htmlWithDeletedTag = styleAsDeleted ? `${html}` : html; - const htmlContent = containsOnlyEmojis ? Str.replaceAll(htmlWithDeletedTag, '', '') : htmlWithDeletedTag; + let htmlContent = htmlWithDeletedTag; + + if (containsOnlyEmojis) { + htmlContent = Str.replaceAll(htmlWithDeletedTag, '', ''); + } else if (CONST.REGEX.ALL_EMOJIS.test(text ?? '')) { + htmlContent = Str.replaceAll(htmlWithDeletedTag, '', ''); + } + let htmlWithTag = editedTag ? `${htmlContent}${editedTag}` : htmlContent; if (styleAsMuted) { @@ -73,24 +81,36 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so const message = isEmpty(iouMessage) ? text : iouMessage; + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); + return ( - - {convertToLTR(message ?? '')} - + {processedTextArray.length !== 0 ? ( + + ) : ( + + {convertToLTR(message ?? '')} + + )} {fragment?.isEdited && ( <> ; + + /** Should this message fragment be styled as deleted? */ + styleAsDeleted?: boolean; + + /** Should this message fragment be styled as muted? */ + styleAsMuted?: boolean; + + /** Does message contain only emojis? */ + hasEmojisOnly?: boolean; +}; + +function TextWithEmojiFragment({message, passedStyles, styleAsDeleted, styleAsMuted, hasEmojisOnly}: TextWithEmojiFragmentProps) { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); + + return ( + + {processedTextArray.map(({text: textik, isEmoji}) => + isEmoji ? ( + + {textik} + + ) : ( + convertToLTR(textik ?? '') + ), + )} + + ); +} + +TextWithEmojiFragment.displayName = 'TextWithEmojiFragment'; + +export default TextWithEmojiFragment; diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx new file mode 100644 index 000000000000..bff42cd15b24 --- /dev/null +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx @@ -0,0 +1,51 @@ +import React, {useMemo} from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; +import Text from '@components/Text'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import convertToLTR from '@libs/convertToLTR'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import * as EmojiUtils from '@libs/EmojiUtils'; + +type TextWithEmojiFragmentProps = { + /** The message to be displayed */ + message: string; + + /** Additional styles to add after local styles. */ + passedStyles?: StyleProp; + + /** Should this message fragment be styled as deleted? */ + styleAsDeleted?: boolean; + + /** Should this message fragment be styled as muted? */ + styleAsMuted?: boolean; + + /** Does message contain only emojis? */ + hasEmojisOnly?: boolean; +}; + +function TextWithEmojiFragment({message, passedStyles, styleAsDeleted, styleAsMuted, hasEmojisOnly}: TextWithEmojiFragmentProps) { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); + + return ( + + {processedTextArray.map(({text: textik, isEmoji}) => (isEmoji ? {textik} : convertToLTR(textik ?? '')))} + + ); +} + +TextWithEmojiFragment.displayName = 'TextWithEmojiFragment'; + +export default TextWithEmojiFragment; diff --git a/src/styles/index.ts b/src/styles/index.ts index b3cb5b624bac..d6e52eca8348 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -362,6 +362,9 @@ const styles = (theme: ThemeColors) => textAlign: 'left', }, + verticalAlignMiddle: { + verticalAlign: 'middle', + }, verticalAlignTop: { verticalAlign: 'top', }, @@ -1710,6 +1713,10 @@ const styles = (theme: ThemeColors) => lineHeight: variables.fontSizeOnlyEmojisHeight, }, + emojisWithTextFontSize: { + fontSize: variables.fontSizeEmojisWithinText, + }, + createMenuPositionSidebar: (windowHeight: number) => ({ horizontal: 18, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index e0720ad1d836..624134afa179 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -48,8 +48,6 @@ export default { defaultAvatarPreviewSize: 360, fabBottom: 25, breadcrumbsFontSize: getValueUsingPixelRatio(19, 32), - fontSizeOnlyEmojis: 30, - fontSizeOnlyEmojisHeight: 35, fontSizeSmall: getValueUsingPixelRatio(11, 17), fontSizeExtraSmall: 9, fontSizeLabel: getValueUsingPixelRatio(13, 19), @@ -87,8 +85,6 @@ export default { sidebarAvatarSize: 28, iconHeader: 48, iconSection: 68, - emojiSize: 20, - emojiLineHeight: 28, iouAmountTextSize: 40, extraSmallMobileResponsiveWidthBreakpoint: 320, extraSmallMobileResponsiveHeightBreakpoint: 667, @@ -213,6 +209,13 @@ export default { welcomeVideoDelay: 1000, explanationModalDelay: 2000, + // Emoji related variables + fontSizeOnlyEmojis: 30, + fontSizeOnlyEmojisHeight: 35, + emojiSize: 20, + emojiLineHeight: 28, + fontSizeEmojisWithinText: 19, + // The height of the empty list is 14px (2px for borders and 12px for vertical padding) // This is calculated based on the values specified in the 'getGoogleListViewStyle' function of the 'StyleUtils' utility googleEmptyListViewHeight: 14, From 8fdd879c32d4d2a493d6ea72951d645e1805fa27 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 8 Aug 2024 11:56:33 +0200 Subject: [PATCH 004/352] Add more options for testing --- .../TextWithEmojiFragment/index.native.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx index 365308ea9e7b..67a8055659c6 100644 --- a/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx @@ -1,5 +1,6 @@ import React, {useMemo} from 'react'; -import {StyleProp, TextStyle, View} from 'react-native'; +import {View} from 'react-native'; +import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -41,15 +42,21 @@ function TextWithEmojiFragment({message, passedStyles, styleAsDeleted, styleAsMu !DeviceCapabilities.canUseTouchScreen() || !shouldUseNarrowLayout ? styles.userSelectText : styles.userSelectNone, ]} > - {processedTextArray.map(({text: textik, isEmoji}) => + {processedTextArray.map(({text: textItem, isEmoji}) => isEmoji ? ( - {textik} + {textItem} ) : ( - convertToLTR(textik ?? '') + convertToLTR(textItem ?? '') ), )} + + {/* Option 2 */} + {/* {processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? {textItem} : convertToLTR(textItem ?? '')))} */} + + {/* Option 3 - with 15 font size */} + {/* {processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? {textItem} : convertToLTR(textItem ?? '')))} */} ); } From 1a776abfac0b86940e50956d271802cb610a65fc Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 12 Aug 2024 11:31:24 +0200 Subject: [PATCH 005/352] Improve messages with enlarged emojis display --- .../HTMLRenderers/EmojiRenderer.tsx | 2 +- src/hooks/useMarkdownStyle.ts | 2 +- .../index.native.tsx | 2 +- .../index.tsx | 4 +- .../report/comment/TextCommentFragment.tsx | 22 +++++---- .../TextWithEmojiFragment/index.native.tsx | 48 +++---------------- .../comment/TextWithEmojiFragment/index.tsx | 40 ++-------------- .../comment/TextWithEmojiFragment/types.ts | 11 +++++ src/styles/index.ts | 9 +++- src/styles/variables.ts | 3 +- 10 files changed, 48 insertions(+), 95 deletions(-) create mode 100644 src/pages/home/report/comment/TextWithEmojiFragment/types.ts diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx index 2764f4edbe7e..023ca07beb63 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -11,7 +11,7 @@ function EmojiRenderer({tnode}: CustomRendererProps) { } if ('ismedium' in tnode.attributes) { - return [styles.emojisWithTextFontSize, styles.verticalAlignMiddle]; + return [styles.emojisWithTextFontSizeXLarge, styles.verticalAlignMiddle]; } return null; diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index c7e9bf2c0218..1a04bddd31a6 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -10,7 +10,7 @@ const defaultEmptyArray: Array = []; function useMarkdownStyle(message: string | null = null, excludeStyles: Array = defaultEmptyArray): MarkdownStyle { const theme = useTheme(); const hasMessageOnlyEmojis = message != null && message.length > 0 && containsOnlyEmojis(message); - const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeNormal; + const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeEmojisWithinTextLarge; // this map is used to reset the styles that are not needed - passing undefined value can break the native side const nonStylingDefaultValues: Record = useMemo( diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx index 94da845f4030..bb00418772eb 100644 --- a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx @@ -38,7 +38,7 @@ function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateA {processedTextArray.map(({text, isEmoji}) => isEmoji ? ( - 😁 + 😁 ) : ( - {processedTextArray.length !== 0 ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) : fragmentText} + {processedTextArray.length !== 0 + ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) + : fragmentText} ); diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 4722271492e7..32ffa4c64ed1 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -49,6 +49,10 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const message = isEmpty(iouMessage) ? text : iouMessage; + + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); + // If the only difference between fragment.text and fragment.html is
tags and emoji tag // on native, we render it as text, not as html // on other device, only render it as text if the only difference is
tag @@ -79,23 +83,23 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so ); } - const message = isEmpty(iouMessage) ? text : iouMessage; - - const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); - return ( - {processedTextArray.length !== 0 ? ( + {processedTextArray.length !== 0 && !containsOnlyEmojis ? ( ) : ( ; - - /** Should this message fragment be styled as deleted? */ - styleAsDeleted?: boolean; - - /** Should this message fragment be styled as muted? */ - styleAsMuted?: boolean; - - /** Does message contain only emojis? */ - hasEmojisOnly?: boolean; -}; - -function TextWithEmojiFragment({message, passedStyles, styleAsDeleted, styleAsMuted, hasEmojisOnly}: TextWithEmojiFragmentProps) { +function TextWithEmojiFragment({message = '', style}: TextWithEmojiFragmentProps) { const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); return ( - - {processedTextArray.map(({text: textItem, isEmoji}) => + + {processedTextArray.map(({text, isEmoji}) => isEmoji ? ( - {textItem} + {text} ) : ( - convertToLTR(textItem ?? '') + convertToLTR(text) ), )} - - {/* Option 2 */} - {/* {processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? {textItem} : convertToLTR(textItem ?? '')))} */} - - {/* Option 3 - with 15 font size */} - {/* {processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? {textItem} : convertToLTR(textItem ?? '')))} */} ); } diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx index bff42cd15b24..4bbb16dafc48 100644 --- a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx @@ -1,49 +1,15 @@ import React, {useMemo} from 'react'; -import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import * as EmojiUtils from '@libs/EmojiUtils'; +import type TextWithEmojiFragmentProps from './types'; -type TextWithEmojiFragmentProps = { - /** The message to be displayed */ - message: string; - - /** Additional styles to add after local styles. */ - passedStyles?: StyleProp; - - /** Should this message fragment be styled as deleted? */ - styleAsDeleted?: boolean; - - /** Should this message fragment be styled as muted? */ - styleAsMuted?: boolean; - - /** Does message contain only emojis? */ - hasEmojisOnly?: boolean; -}; - -function TextWithEmojiFragment({message, passedStyles, styleAsDeleted, styleAsMuted, hasEmojisOnly}: TextWithEmojiFragmentProps) { +function TextWithEmojiFragment({message = '', style}: TextWithEmojiFragmentProps) { const styles = useThemeStyles(); - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); - return ( - - {processedTextArray.map(({text: textik, isEmoji}) => (isEmoji ? {textik} : convertToLTR(textik ?? '')))} - - ); + return {processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : convertToLTR(text)))}; } TextWithEmojiFragment.displayName = 'TextWithEmojiFragment'; diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/types.ts b/src/pages/home/report/comment/TextWithEmojiFragment/types.ts new file mode 100644 index 000000000000..243b02f1fd76 --- /dev/null +++ b/src/pages/home/report/comment/TextWithEmojiFragment/types.ts @@ -0,0 +1,11 @@ +import type {StyleProp, TextStyle} from 'react-native'; + +type TextWithEmojiFragmentProps = { + /** The message to be displayed */ + message?: string; + + /** Any additional styles to apply */ + style: StyleProp; +}; + +export default TextWithEmojiFragmentProps; diff --git a/src/styles/index.ts b/src/styles/index.ts index d6e52eca8348..1592a66f5dbb 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1713,8 +1713,13 @@ const styles = (theme: ThemeColors) => lineHeight: variables.fontSizeOnlyEmojisHeight, }, - emojisWithTextFontSize: { - fontSize: variables.fontSizeEmojisWithinText, + emojisWithTextFontSizeLarge: { + fontSize: variables.fontSizeEmojisWithinTextLarge, + marginVertical: -7, + }, + + emojisWithTextFontSizeXLarge: { + fontSize: variables.fontSizeEmojisWithinTextXLarge, }, createMenuPositionSidebar: (windowHeight: number) => diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 624134afa179..dba6c7a0681b 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -214,7 +214,8 @@ export default { fontSizeOnlyEmojisHeight: 35, emojiSize: 20, emojiLineHeight: 28, - fontSizeEmojisWithinText: 19, + fontSizeEmojisWithinTextLarge: getValueUsingPixelRatio(17, 19), + fontSizeEmojisWithinTextXLarge: 19, // The height of the empty list is 14px (2px for borders and 12px for vertical padding) // This is calculated based on the values specified in the 'getGoogleListViewStyle' function of the 'StyleUtils' utility From 957737a9545ba51bb183f370249aa3a4ae68d650 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 12 Aug 2024 14:09:50 +0200 Subject: [PATCH 006/352] Code clean up --- .../HTMLRenderers/EmojiRenderer.tsx | 3 +- src/hooks/useMarkdownStyle.ts | 3 +- .../index.native.tsx | 53 ++++--------------- .../index.tsx | 23 +------- .../types.ts | 20 +++++++ .../TextWithEmojiFragment/index.native.tsx | 2 +- .../comment/TextWithEmojiFragment/index.tsx | 2 +- src/styles/index.ts | 16 ++++-- src/styles/variables.ts | 3 +- 9 files changed, 50 insertions(+), 75 deletions(-) create mode 100644 src/pages/home/report/ReportActionItemMessageHeaderSender/types.ts diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx index 023ca07beb63..7f087db98e14 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -11,7 +11,8 @@ function EmojiRenderer({tnode}: CustomRendererProps) { } if ('ismedium' in tnode.attributes) { - return [styles.emojisWithTextFontSizeXLarge, styles.verticalAlignMiddle]; + // TODO: Think about other approaches to align text selection {lineHeight: 22, marginTop: -2} + return [styles.emojisWithTextFontSize, styles.verticalAlignMiddle, {lineHeight: 22, marginTop: -2}]; } return null; diff --git a/src/hooks/useMarkdownStyle.ts b/src/hooks/useMarkdownStyle.ts index 1a04bddd31a6..8acc766a469c 100644 --- a/src/hooks/useMarkdownStyle.ts +++ b/src/hooks/useMarkdownStyle.ts @@ -10,7 +10,7 @@ const defaultEmptyArray: Array = []; function useMarkdownStyle(message: string | null = null, excludeStyles: Array = defaultEmptyArray): MarkdownStyle { const theme = useTheme(); const hasMessageOnlyEmojis = message != null && message.length > 0 && containsOnlyEmojis(message); - const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeEmojisWithinTextLarge; + const emojiFontSize = hasMessageOnlyEmojis ? variables.fontSizeOnlyEmojis : variables.fontSizeEmojisWithinText; // this map is used to reset the styles that are not needed - passing undefined value can break the native side const nonStylingDefaultValues: Record = useMemo( @@ -38,6 +38,7 @@ function useMarkdownStyle(message: string | null = null, excludeStyles: Array - {processedTextArray.length !== 0 ? ( - - {processedTextArray.map(({text, isEmoji}) => - isEmoji ? ( - - 😁 - - ) : ( - - {text} - - ), - )} - - ) : ( - - {fragmentText} - - )} + + {processedTextArray.length !== 0 + ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) + : fragmentText} + ); } diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx index 8b0b31f445c4..e86087ee58c3 100644 --- a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx @@ -3,24 +3,7 @@ import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; -import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; - -type ReportActionItemMessageHeaderSenderProps = { - /** Text to display */ - fragmentText: string; - - /** Users accountID */ - accountID: number; - - /** Should this fragment be contained in a single line? */ - isSingleLine?: boolean; - - /** The accountID of the copilot who took this action on behalf of the user */ - delegateAccountID?: number; - - /** Actor icon */ - actorIcon?: OnyxCommon.Icon; -}; +import type ReportActionItemMessageHeaderSenderProps from './types'; function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateAccountID, actorIcon, isSingleLine}: ReportActionItemMessageHeaderSenderProps) { const styles = useThemeStyles(); @@ -36,9 +19,7 @@ function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateA numberOfLines={isSingleLine ? 1 : undefined} style={[styles.chatItemMessageHeaderSender, isSingleLine ? styles.pre : styles.preWrap, styles.dFlex]} > - {processedTextArray.length !== 0 - ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) - : fragmentText} + {processedTextArray.length !== 0 ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) : fragmentText} ); diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/types.ts b/src/pages/home/report/ReportActionItemMessageHeaderSender/types.ts new file mode 100644 index 000000000000..44a27de119e6 --- /dev/null +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/types.ts @@ -0,0 +1,20 @@ +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; + +type ReportActionItemMessageHeaderSenderProps = { + /** Text to display */ + fragmentText: string; + + /** Users accountID */ + accountID: number; + + /** Should this fragment be contained in a single line? */ + isSingleLine?: boolean; + + /** The accountID of the copilot who took this action on behalf of the user */ + delegateAccountID?: number; + + /** Actor icon */ + actorIcon?: OnyxCommon.Icon; +}; + +export default ReportActionItemMessageHeaderSenderProps; diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx index 47133a1def0a..f4efeefc6623 100644 --- a/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx @@ -15,7 +15,7 @@ function TextWithEmojiFragment({message = '', style}: TextWithEmojiFragmentProps {processedTextArray.map(({text, isEmoji}) => isEmoji ? ( - {text} + {text} ) : ( convertToLTR(text) diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx index 4bbb16dafc48..e21a53451f2b 100644 --- a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx @@ -9,7 +9,7 @@ function TextWithEmojiFragment({message = '', style}: TextWithEmojiFragmentProps const styles = useThemeStyles(); const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); - return {processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : convertToLTR(text)))}; + return {processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : convertToLTR(text)))}; } TextWithEmojiFragment.displayName = 'TextWithEmojiFragment'; diff --git a/src/styles/index.ts b/src/styles/index.ts index 1592a66f5dbb..480b4aaedf81 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1713,13 +1713,21 @@ const styles = (theme: ThemeColors) => lineHeight: variables.fontSizeOnlyEmojisHeight, }, - emojisWithTextFontSizeLarge: { - fontSize: variables.fontSizeEmojisWithinTextLarge, + emojisWithTextFontSizeAligned: { + fontSize: variables.fontSizeEmojisWithinText, marginVertical: -7, }, - emojisWithTextFontSizeXLarge: { - fontSize: variables.fontSizeEmojisWithinTextXLarge, + emojisWithTextFontSize: { + fontSize: variables.fontSizeEmojisWithinText, + }, + + emojisWithTextFontFamily: { + fontFamily: FontUtils.fontFamily.platform.SYSTEM.fontFamily, + }, + + emojisWithTextLineHeight: { + lineHeight: variables.lineHeightXLarge, }, createMenuPositionSidebar: (windowHeight: number) => diff --git a/src/styles/variables.ts b/src/styles/variables.ts index dba6c7a0681b..39ff79f9c914 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -214,8 +214,7 @@ export default { fontSizeOnlyEmojisHeight: 35, emojiSize: 20, emojiLineHeight: 28, - fontSizeEmojisWithinTextLarge: getValueUsingPixelRatio(17, 19), - fontSizeEmojisWithinTextXLarge: 19, + fontSizeEmojisWithinText: getValueUsingPixelRatio(17, 19), // The height of the empty list is 14px (2px for borders and 12px for vertical padding) // This is calculated based on the values specified in the 'getGoogleListViewStyle' function of the 'StyleUtils' utility From 27fb058652f91c912e0b57279d8cc614a16c4532 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 14 Aug 2024 16:32:51 +0200 Subject: [PATCH 007/352] Fix composer height when only emojis are entered --- src/components/Composer/index.native.tsx | 6 +++++- src/components/Composer/index.tsx | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 68a8c56c4df9..e5d20ebfb294 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -24,6 +24,7 @@ function Composer( maxLines, isComposerFullSize = false, setIsFullComposerAvailable = () => {}, + isFullComposerAvailable = false, autoFocus = false, style, // On native layers we like to have the Text Input not focused so the @@ -71,7 +72,10 @@ function Composer( ); const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); - const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]); + const composerStyle = useMemo( + () => StyleSheet.flatten([style, textContainsOnlyEmojis && isFullComposerAvailable ? styles.onlyEmojisTextLineHeight : {}]), + [style, textContainsOnlyEmojis, isFullComposerAvailable, styles], + ); return ( Date: Wed, 14 Aug 2024 17:23:31 +0200 Subject: [PATCH 008/352] Increase emojis in the display name --- src/pages/settings/InitialSettingsPage.tsx | 35 ++++++++++++------- .../settings/Profile/DisplayNamePage.tsx | 2 ++ src/styles/index.ts | 4 +++ src/styles/variables.ts | 1 + 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx index bc98a9432630..648690505dc0 100755 --- a/src/pages/settings/InitialSettingsPage.tsx +++ b/src/pages/settings/InitialSettingsPage.tsx @@ -29,6 +29,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as EmojiUtils from '@libs/EmojiUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as SubscriptionUtils from '@libs/SubscriptionUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -364,9 +365,10 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa const generalMenuItems = useMemo(() => getMenuItemsSection(generalMenuItemsData), [generalMenuItemsData, getMenuItemsSection]); const workspaceMenuItems = useMemo(() => getMenuItemsSection(workspaceMenuItemsData), [workspaceMenuItemsData, getMenuItemsSection]); - const currentUserDetails = currentUserPersonalDetails; - const avatarURL = currentUserDetails?.avatar ?? ''; - const accountID = currentUserDetails?.accountID ?? '-1'; + const avatarURL = currentUserPersonalDetails?.avatar ?? ''; + const accountID = currentUserPersonalDetails?.accountID ?? '-1'; + + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(currentUserPersonalDetails?.displayName), [currentUserPersonalDetails]); const headerContent = ( @@ -416,7 +418,7 @@ function InitialSettingsPage({session, userWallet, bankAccountList, fundList, wa Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))} previewSource={UserUtils.getFullSizeAvatar(avatarURL, accountID)} - originalFileName={currentUserDetails.originalFileName} + originalFileName={currentUserPersonalDetails?.originalFileName} headerTitle={translate('profilePage.profileAvatar')} - fallbackIcon={currentUserDetails?.fallbackIcon} + fallbackIcon={currentUserPersonalDetails?.fallbackIcon} editIconStyle={styles.smallEditIconAccount} /> - - {currentUserPersonalDetails.displayName ? currentUserPersonalDetails.displayName : formatPhoneNumber(session?.email ?? '')} - + {processedTextArray.length !== 0 ? ( + + {processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text))} + + ) : ( + + {currentUserPersonalDetails.displayName ? currentUserPersonalDetails.displayName : formatPhoneNumber(session?.email ?? '')} + + )} {!!currentUserPersonalDetails.displayName && ( @@ -114,6 +115,7 @@ function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: Disp role={CONST.ROLE.PRESENTATION} defaultValue={currentUserDetails.lastName ?? ''} spellCheck={false} + isMarkdownEnabled /> diff --git a/src/styles/index.ts b/src/styles/index.ts index 48aa36cfa1a8..8ac9be222799 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1730,6 +1730,10 @@ const styles = (theme: ThemeColors) => lineHeight: variables.lineHeightXLarge, }, + initialSettingsUsernameEmoji: { + fontSize: variables.fontSizeUsernameEmoji, + }, + createMenuPositionSidebar: (windowHeight: number) => ({ horizontal: 18, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 87444ee23c58..ef8818cb939f 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -214,6 +214,7 @@ export default { fontSizeOnlyEmojisHeight: 35, emojiSize: 20, emojiLineHeight: 28, + fontSizeUsernameEmoji: 25, fontSizeEmojisWithinText: getValueUsingPixelRatio(17, 19), // The height of the empty list is 14px (2px for borders and 12px for vertical padding) From e75050100724094cbfb62589d69e6ce230556413 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 16 Aug 2024 13:45:01 +0200 Subject: [PATCH 009/352] Fix emojis are cut off in some places on ios --- src/components/SelectionList/Search/UserInfoCell.tsx | 2 +- src/components/TextWithTooltip/index.native.tsx | 11 +++++++++-- src/styles/index.ts | 6 +++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/components/SelectionList/Search/UserInfoCell.tsx b/src/components/SelectionList/Search/UserInfoCell.tsx index 793c8c94c391..064033e6dcdc 100644 --- a/src/components/SelectionList/Search/UserInfoCell.tsx +++ b/src/components/SelectionList/Search/UserInfoCell.tsx @@ -30,7 +30,7 @@ function UserInfoCell({participant, displayName}: UserInfoCellProps) { /> {displayName} diff --git a/src/components/TextWithTooltip/index.native.tsx b/src/components/TextWithTooltip/index.native.tsx index b857ded2588b..bb3d9052b5b7 100644 --- a/src/components/TextWithTooltip/index.native.tsx +++ b/src/components/TextWithTooltip/index.native.tsx @@ -1,14 +1,21 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; import type TextWithTooltipProps from './types'; function TextWithTooltip({text, style, numberOfLines = 1}: TextWithTooltipProps) { + const styles = useThemeStyles(); + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(text), [text]); + return ( - {text} + {processedTextArray.length !== 0 + ? processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? {textItem} : textItem)) + : text} ); } diff --git a/src/styles/index.ts b/src/styles/index.ts index a73a3cf277f1..aefa1287a5eb 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -413,7 +413,7 @@ const styles = (theme: ThemeColors) => color: theme.text, ...FontUtils.fontFamily.platform.EXP_NEUE_BOLD, fontSize: variables.fontSizeSmall, - lineHeight: variables.lineHeightSmall, + lineHeight: variables.lineHeightNormal, }, textMicroSupporting: { @@ -1718,6 +1718,10 @@ const styles = (theme: ThemeColors) => marginVertical: -7, }, + emojisFontFamily: { + fontFamily: FontUtils.fontFamily.platform.SYSTEM.fontFamily, + }, + emojisWithTextFontSize: { fontSize: variables.fontSizeEmojisWithinText, }, From 3175d40c4b773f6abe5207eb526bc7d28f40d89f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 16 Aug 2024 13:46:50 +0200 Subject: [PATCH 010/352] Lint fixes --- src/components/Composer/index.tsx | 2 +- src/pages/home/report/ReportActionItemFragment.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 15cfb3831348..b204513f5d93 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -383,7 +383,7 @@ function Composer( textContainsOnlyEmojis && isFullComposerAvailable ? styles.onlyEmojisTextLineHeight : {}, ], - [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], + [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, isFullComposerAvailable, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], ); return ( diff --git a/src/pages/home/report/ReportActionItemFragment.tsx b/src/pages/home/report/ReportActionItemFragment.tsx index 64b88045e385..05cb657b1e54 100644 --- a/src/pages/home/report/ReportActionItemFragment.tsx +++ b/src/pages/home/report/ReportActionItemFragment.tsx @@ -7,7 +7,6 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import convertToLTR from '@libs/convertToLTR'; import * as ReportUtils from '@libs/ReportUtils'; -import ReportActionItemMessageHeaderSender from '@pages/home/report/ReportActionItemMessageHeaderSender'; import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {DecisionName, OriginalMessageSource} from '@src/types/onyx/OriginalMessage'; @@ -15,6 +14,7 @@ import type {Message} from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import AttachmentCommentFragment from './comment/AttachmentCommentFragment'; import TextCommentFragment from './comment/TextCommentFragment'; +import ReportActionItemMessageHeaderSender from './ReportActionItemMessageHeaderSender'; type ReportActionItemFragmentProps = { /** Users accountID */ From 8b0828b0925e22dd0729010d88fe98e1915f2bd6 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 16 Aug 2024 14:08:39 +0200 Subject: [PATCH 011/352] Try to fix react compiler error --- src/components/TextWithTooltip/index.native.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TextWithTooltip/index.native.tsx b/src/components/TextWithTooltip/index.native.tsx index bb3d9052b5b7..892b34649d93 100644 --- a/src/components/TextWithTooltip/index.native.tsx +++ b/src/components/TextWithTooltip/index.native.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -6,7 +6,7 @@ import type TextWithTooltipProps from './types'; function TextWithTooltip({text, style, numberOfLines = 1}: TextWithTooltipProps) { const styles = useThemeStyles(); - const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(text), [text]); + const processedTextArray = EmojiUtils.splitTextWithEmojis(text); return ( Date: Mon, 19 Aug 2024 15:48:23 +0200 Subject: [PATCH 012/352] Resolve TODO related to the text selection --- .../HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx | 3 +-- src/styles/index.ts | 3 +++ src/styles/utils/emojiDefaultStyles/index.ts | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx index 7f087db98e14..5d359b6f54a8 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -11,8 +11,7 @@ function EmojiRenderer({tnode}: CustomRendererProps) { } if ('ismedium' in tnode.attributes) { - // TODO: Think about other approaches to align text selection {lineHeight: 22, marginTop: -2} - return [styles.emojisWithTextFontSize, styles.verticalAlignMiddle, {lineHeight: 22, marginTop: -2}]; + return [styles.emojisWithTextFontSize, styles.verticalAlignTopText]; } return null; diff --git a/src/styles/index.ts b/src/styles/index.ts index aefa1287a5eb..a9351770890f 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -365,6 +365,9 @@ const styles = (theme: ThemeColors) => verticalAlignMiddle: { verticalAlign: 'middle', }, + verticalAlignTopText: { + verticalAlign: 'top-text', + }, verticalAlignTop: { verticalAlign: 'top', }, diff --git a/src/styles/utils/emojiDefaultStyles/index.ts b/src/styles/utils/emojiDefaultStyles/index.ts index 88c42e7e95d1..45880b46005d 100644 --- a/src/styles/utils/emojiDefaultStyles/index.ts +++ b/src/styles/utils/emojiDefaultStyles/index.ts @@ -6,7 +6,7 @@ import type EmojiDefaultStyles from './types'; const emojiDefaultStyles: EmojiDefaultStyles = { fontStyle: 'normal', fontWeight: FontUtils.fontWeight.normal, - ...display.dInlineFlex, + ...display.dInline, }; export default emojiDefaultStyles; From 41ffb05edbeb903abc763cc2f675a5f03965bea1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 20 Aug 2024 14:45:15 +0200 Subject: [PATCH 013/352] Fix cursor jumping on ios --- src/components/TextInput/BaseTextInput/index.native.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 59f205da023f..ffe30f4169f0 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -179,9 +179,10 @@ function BaseTextInput( } const layout = event.nativeEvent.layout; + const HEIGHT_TO_FIT_EMOJIS = 1; setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); + setHeight((prevHeight: number) => (!multiline ? layout.height + HEIGHT_TO_FIT_EMOJIS : prevHeight)); }, [autoGrowHeight, multiline], ); From 2007c58a43ff6f4b9f6fc000e6e40856346a83e4 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 20 Aug 2024 15:15:10 +0200 Subject: [PATCH 014/352] Fix emojis are cut off in the workspace list on ios --- src/pages/workspace/WorkspacesListRow.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index ac53252829fa..5e23e88cb167 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -16,6 +16,7 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import type {AvatarSource} from '@libs/UserUtils'; import type {AnchorPosition} from '@styles/index'; @@ -116,6 +117,8 @@ function WorkspacesListRow({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const ownerDetails = ownerAccountID && PersonalDetailsUtils.getPersonalDetailsByIDs([ownerAccountID], currentUserPersonalDetails.accountID)[0]; + const ownerName = ownerDetails ? PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails) : ''; + const processedOwnerName = EmojiUtils.splitTextWithEmojis(ownerName); const userFriendlyWorkspaceType = useMemo(() => { switch (workspaceType) { @@ -221,7 +224,15 @@ function WorkspacesListRow({ numberOfLines={1} style={[styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}]} > - {PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)} + {processedOwnerName.length !== 0 + ? processedOwnerName.map(({text, isEmoji}) => + isEmoji ? ( + {text} + ) : ( + text + ), + ) + : ownerName} Date: Tue, 20 Aug 2024 16:42:57 +0200 Subject: [PATCH 015/352] Fix composer height --- src/libs/ComposerUtils/updateIsFullComposerAvailable.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts index c10635f1c491..eb83f5c06467 100644 --- a/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts +++ b/src/libs/ComposerUtils/updateIsFullComposerAvailable.ts @@ -14,7 +14,7 @@ function updateIsFullComposerAvailable(props: ComposerProps, event: NativeSynthe return; } const totalHeight = inputHeight + paddingTopAndBottom; - const isFullComposerAvailable = totalHeight >= CONST.COMPOSER.FULL_COMPOSER_MIN_HEIGHT; + const isFullComposerAvailable = totalHeight > CONST.COMPOSER.FULL_COMPOSER_MIN_HEIGHT; if (isFullComposerAvailable !== props.isFullComposerAvailable) { props.setIsFullComposerAvailable?.(isFullComposerAvailable); } From 2dd77732a011d96c2db147ce11d5160b2365a701 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 20 Aug 2024 17:29:55 +0200 Subject: [PATCH 016/352] Lint fix --- src/styles/index.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index 609af3708ea2..1e157209a696 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -362,9 +362,6 @@ const styles = (theme: ThemeColors) => textAlign: 'left', }, - verticalAlignMiddle: { - verticalAlign: 'middle', - }, verticalAlignTopText: { verticalAlign: 'top-text', }, From cc8e8b173fff498db2845470432c9e0b23c730f5 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 21 Aug 2024 10:59:50 +0200 Subject: [PATCH 017/352] Fix large emojis overlap --- src/components/Composer/index.native.tsx | 14 +++++++++----- src/components/Composer/index.tsx | 8 +++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index 2b6644cedceb..6f46699673e3 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,6 +1,6 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useMemo, useRef} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native'; import {StyleSheet} from 'react-native'; import type {FileObject} from '@components/AttachmentModal'; @@ -13,6 +13,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as EmojiUtils from '@libs/EmojiUtils'; +import variables from '@styles/variables'; import type {ComposerProps} from './types'; const excludeNoStyles: Array = []; @@ -26,7 +27,6 @@ function Composer( maxLines, isComposerFullSize = false, setIsFullComposerAvailable = () => {}, - isFullComposerAvailable = false, autoFocus = false, style, // On native layers we like to have the Text Input not focused so the @@ -40,6 +40,7 @@ function Composer( ref: ForwardedRef, ) { const textInput = useRef(null); + const [hasMultipleLines, setHasMultipleLines] = useState(false); const {isFocused, shouldResetFocusRef} = useResetComposerFocus(textInput); const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]); const theme = useTheme(); @@ -89,8 +90,8 @@ function Composer( const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); const composerStyle = useMemo( - () => StyleSheet.flatten([style, textContainsOnlyEmojis && isFullComposerAvailable ? styles.onlyEmojisTextLineHeight : {}]), - [style, textContainsOnlyEmojis, isFullComposerAvailable, styles], + () => StyleSheet.flatten([style, textContainsOnlyEmojis && hasMultipleLines ? styles.onlyEmojisTextLineHeight : {}]), + [style, textContainsOnlyEmojis, hasMultipleLines, styles], ); return ( @@ -100,7 +101,10 @@ function Composer( placeholderTextColor={theme.placeholderText} ref={setTextInputRef} value={value} - onContentSizeChange={(e) => updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true)} + onContentSizeChange={(e) => { + setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge); + updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true); + }} rejectResponderTermination={false} smartInsertDelete={false} textAlignVertical="center" diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index e0a98ec80010..2847c6cc615b 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -21,6 +21,7 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import type {ComposerProps} from './types'; @@ -72,7 +73,6 @@ function Composer( end: 0, }, isReportActionCompose = false, - isFullComposerAvailable = false, isComposerFullSize = false, shouldContainScroll = true, isGroupPolicyReport = false, @@ -102,6 +102,7 @@ function Composer( const [caretContent, setCaretContent] = useState(''); const [valueBeforeCaret, setValueBeforeCaret] = useState(''); const [textInputWidth, setTextInputWidth] = useState(''); + const [hasMultipleLines, setHasMultipleLines] = useState(false); const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); @@ -380,10 +381,10 @@ function Composer( scrollStyleMemo, StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), isComposerFullSize ? {height: '100%', maxHeight: 'none'} : undefined, - textContainsOnlyEmojis && isFullComposerAvailable ? styles.onlyEmojisTextLineHeight : {}, + textContainsOnlyEmojis && hasMultipleLines ? styles.onlyEmojisTextLineHeight : {}, ], - [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, isFullComposerAvailable, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], + [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, hasMultipleLines, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], ); return ( @@ -403,6 +404,7 @@ function Composer( {...props} onSelectionChange={addCursorPositionToSelectionChange} onContentSizeChange={(e) => { + setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge); setTextInputWidth(`${e.nativeEvent.contentSize.width}px`); updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); }} From 435e777a3226436004143e0f6331741e2379aa7a Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 21 Aug 2024 11:40:26 +0200 Subject: [PATCH 018/352] Improve sender display --- .../home/report/ReportActionItemMessageHeaderSender/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx index e86087ee58c3..d2bedbd3f18b 100644 --- a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx @@ -17,7 +17,7 @@ function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateA > {processedTextArray.length !== 0 ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) : fragmentText} From 6ef259587b965dbeffad504b177d8696d622c408 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 23 Aug 2024 11:23:01 +0200 Subject: [PATCH 019/352] Lint fix --- src/pages/home/report/comment/TextCommentFragment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 93c884c3acf2..7c39ade32e40 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -1,6 +1,6 @@ import {Str} from 'expensify-common'; import {isEmpty} from 'lodash'; -import React, {memo, useMemo, useEffect} from 'react'; +import React, {memo, useEffect, useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import Text from '@components/Text'; import ZeroWidthView from '@components/ZeroWidthView'; From a428070ae660713f0a7bfab57631b20a4c9b00fe Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:38:39 +0530 Subject: [PATCH 020/352] Update --- tests/ui/GroupChatNameTests.tsx | 50 +-------------------------------- tests/utils/TestHelper.ts | 1 - 2 files changed, 1 insertion(+), 50 deletions(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 7eb70b412f70..fa84ee1e12d5 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -45,55 +45,7 @@ type ListenerMock = { addListener: jest.Mock; }; -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * This can't be moved to a utils file because Jest wants any external function to stay in the scope. - * Details: https://github.com/jestjs/jest/issues/2567 - */ -const createAddListenerMock = (): ListenerMock => { - const transitionEndListeners: Array<() => void> = []; - const triggerTransitionEnd = () => { - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback: () => void) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -}; - -jest.mock('@react-navigation/native', () => { - const actualNav = jest.requireActual('@react-navigation/native'); - const {triggerTransitionEnd, addListener} = createAddListenerMock(); - transitionEndCB = triggerTransitionEnd; - - const useNavigation = () => - ({ - navigate: jest.fn(), - ...actualNav.useNavigation, - getState: () => ({ - routes: [], - }), - addListener, - } as typeof NativeNavigation.useNavigation); - - return { - ...actualNav, - useNavigation, - getState: () => ({ - routes: [], - }), - } as typeof NativeNavigation; -}); +jest.mock('@react-navigation/native'); beforeAll(() => { TestHelper.beforeAllSetupUITests(); diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index b10f165a96be..f80ce747837c 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -1,6 +1,5 @@ import {fireEvent, screen} from '@testing-library/react-native'; import {Str} from 'expensify-common'; -import type {Listener} from 'onfido-sdk-ui/types/shared/EventEmitter'; import {Linking} from 'react-native'; import Onyx from 'react-native-onyx'; import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types'; From e8956bcc2b2c5e77054675f39dc8a740a27f3742 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 16:44:44 +0530 Subject: [PATCH 021/352] Update TestHelper.ts --- tests/utils/TestHelper.ts | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index f80ce747837c..72dbfaafe61f 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -308,32 +308,6 @@ function assertFormDataMatchesObject(formData: FormData, obj: Report) { ).toEqual(expect.objectContaining(obj)); } -/** - * This is a helper function to create a mock for the addListener function of the react-navigation library. - * The reason we need this is because we need to trigger the transitionEnd event in our tests to simulate - * the transitionEnd event that is triggered when the screen transition animation is completed. - * - * @returns An object with two functions: triggerTransitionEnd and addListener - */ -const createAddListenerMock = () => { - const transitionEndListeners: Listener[] = []; - const triggerTransitionEnd = () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-return - transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); - }; - - const addListener = jest.fn().mockImplementation((listener, callback: Listener) => { - if (listener === 'transitionEnd') { - transitionEndListeners.push(callback); - } - return () => { - transitionEndListeners.filter((cb) => cb !== callback); - }; - }); - - return {triggerTransitionEnd, addListener}; -}; - async function navigateToSidebarOption(index: number): Promise { const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); const optionRows = screen.queryAllByAccessibilityHint(hintText); @@ -375,7 +349,6 @@ export { expectAPICommandToHaveBeenCalled, expectAPICommandToHaveBeenCalledWith, setupGlobalFetchMock, - createAddListenerMock, navigateToSidebarOption, beforeAllSetupUITests, }; From 3e2f30e6eacb00fe7d154a859d49574a3f726b09 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:11:44 +0530 Subject: [PATCH 022/352] Update --- tests/unit/ReportUtilsTest.ts | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 334244ec066e..9543e02c3c73 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -996,30 +996,48 @@ describe('ReportUtils', () => { describe('getGroupChatName tests', () => { afterEach(() => Onyx.clear()); + const fourParticipants = [ + {accountID: 1, login: "email1@test.com"}, + {accountID: 2, login: "email2@test.com"}, + {accountID: 3, login: "email3@test.com"}, + {accountID: 4, login: "email4@test.com"}, + ] + + const eightParticipants = [ + {accountID: 1, login: "email1@test.com"}, + {accountID: 2, login: "email2@test.com"}, + {accountID: 3, login: "email3@test.com"}, + {accountID: 4, login: "email4@test.com"}, + {accountID: 5, login: "email5@test.com"}, + {accountID: 6, login: "email6@test.com"}, + {accountID: 7, login: "email7@test.com"}, + {accountID: 8, login: "email8@test.com"}, + ] + describe('When participantAccountIDs is passed to getGroupChatName', () => { it('Should show all participants name if count <= 5 and shouldApplyLimit is false', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4])).toEqual('Four, One, Three, Two'); + expect(ReportUtils.getGroupChatName(fourParticipants)).toEqual('Four, One, Three, Two'); }); it('Should show all participants name if count <= 5 and shouldApplyLimit is true', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('Four, One, Three, Two'); + expect(ReportUtils.getGroupChatName(fourParticipants)).toEqual('Four, One, Three, Two'); }); it('Should show 5 participants name if count > 5 and shouldApplyLimit is true', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], true)).toEqual('Five, Four, One, Three, Two'); + expect(ReportUtils.getGroupChatName(eightParticipants, true)).toEqual('Five, Four, One, Three, Two'); }); it('Should show all participants name if count > 5 and shouldApplyLimit is false', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, fakePersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4, 5, 6, 7, 8], false)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); + expect(ReportUtils.getGroupChatName(eightParticipants, false)).toEqual('Eight, Five, Four, One, Seven, Six, Three, Two'); }); it('Should use correct display name for participants', async () => { await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, participantsPersonalDetails); - expect(ReportUtils.getGroupChatName([1, 2, 3, 4], true)).toEqual('(833) 240-3627, floki@vikings.net, Lagertha, Ragnar'); + expect(ReportUtils.getGroupChatName(fourParticipants, true)).toEqual('(833) 240-3627, floki@vikings.net, Lagertha, Ragnar'); }); }); From 84deb67dd9c5e3003286a160f994e0b7ae40e2c6 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:16:01 +0530 Subject: [PATCH 023/352] Update --- tests/ui/GroupChatNameTests.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index fa84ee1e12d5..25defa2ff283 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -3,7 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import type * as NativeNavigation from '@react-navigation/native'; import {act, render, screen, waitFor} from '@testing-library/react-native'; import React from 'react'; import Onyx from 'react-native-onyx'; @@ -40,11 +39,6 @@ jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ */ let transitionEndCB: () => void; -type ListenerMock = { - triggerTransitionEnd: () => void; - addListener: jest.Mock; -}; - jest.mock('@react-navigation/native'); beforeAll(() => { From 221b3ff1399fb6120b2fa8d7fb8016626b3bf8c3 Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 23 Aug 2024 17:31:49 +0530 Subject: [PATCH 024/352] Lint fixes --- tests/unit/ReportUtilsTest.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index 9543e02c3c73..bf8f48e988ab 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -997,22 +997,22 @@ describe('ReportUtils', () => { afterEach(() => Onyx.clear()); const fourParticipants = [ - {accountID: 1, login: "email1@test.com"}, - {accountID: 2, login: "email2@test.com"}, - {accountID: 3, login: "email3@test.com"}, - {accountID: 4, login: "email4@test.com"}, - ] + {accountID: 1, login: 'email1@test.com'}, + {accountID: 2, login: 'email2@test.com'}, + {accountID: 3, login: 'email3@test.com'}, + {accountID: 4, login: 'email4@test.com'}, + ]; const eightParticipants = [ - {accountID: 1, login: "email1@test.com"}, - {accountID: 2, login: "email2@test.com"}, - {accountID: 3, login: "email3@test.com"}, - {accountID: 4, login: "email4@test.com"}, - {accountID: 5, login: "email5@test.com"}, - {accountID: 6, login: "email6@test.com"}, - {accountID: 7, login: "email7@test.com"}, - {accountID: 8, login: "email8@test.com"}, - ] + {accountID: 1, login: 'email1@test.com'}, + {accountID: 2, login: 'email2@test.com'}, + {accountID: 3, login: 'email3@test.com'}, + {accountID: 4, login: 'email4@test.com'}, + {accountID: 5, login: 'email5@test.com'}, + {accountID: 6, login: 'email6@test.com'}, + {accountID: 7, login: 'email7@test.com'}, + {accountID: 8, login: 'email8@test.com'}, + ]; describe('When participantAccountIDs is passed to getGroupChatName', () => { it('Should show all participants name if count <= 5 and shouldApplyLimit is false', async () => { From 7cf9b772607297081ba64fdb77a9a5a23c27e3b0 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Mon, 26 Aug 2024 17:16:04 +0200 Subject: [PATCH 025/352] Fix emoji alignment --- src/styles/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/index.ts b/src/styles/index.ts index ea9162322b2f..6176b7c1734b 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -363,7 +363,7 @@ const styles = (theme: ThemeColors) => }, verticalAlignTopText: { - verticalAlign: 'top-text', + verticalAlign: 'text-top', }, verticalAlignTop: { verticalAlign: 'top', From 272a32dae00750f45812d6a24ba630af99f0fc58 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 29 Aug 2024 17:07:32 +0200 Subject: [PATCH 026/352] Update display name emoji size after merging main --- src/components/AccountSwitcher.tsx | 8 ++++++-- src/styles/variables.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index ba30ea0062b9..428ea27f0fe0 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -1,4 +1,4 @@ -import React, {useRef, useState} from 'react'; +import React, {useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -9,6 +9,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate'; +import * as EmojiUtils from '@libs/EmojiUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import variables from '@styles/variables'; import * as Modal from '@userActions/Modal'; @@ -44,6 +45,7 @@ function AccountSwitcher() { const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate); + const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(currentUserPersonalDetails?.displayName), [currentUserPersonalDetails]); const createBaseMenuItem = (personalDetails: PersonalDetails | undefined, error?: TranslationPaths, additionalProps: MenuItemWithLink = {}): MenuItemWithLink => { return { @@ -143,7 +145,9 @@ function AccountSwitcher() { numberOfLines={1} style={[styles.textBold, styles.textLarge]} > - {currentUserPersonalDetails?.displayName} + {processedTextArray.length !== 0 + ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) + : currentUserPersonalDetails?.displayName} {canSwitchAccounts && ( diff --git a/src/styles/variables.ts b/src/styles/variables.ts index c1ae0646783f..91fd81e6ef44 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -217,7 +217,7 @@ export default { fontSizeOnlyEmojisHeight: 35, emojiSize: 20, emojiLineHeight: 28, - fontSizeUsernameEmoji: 25, + fontSizeUsernameEmoji: 19, fontSizeEmojisWithinText: getValueUsingPixelRatio(17, 19), // The height of the empty list is 14px (2px for borders and 12px for vertical padding) From 87e41e577c02ef18d65079f073fccd431465462b Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 29 Aug 2024 17:15:37 +0200 Subject: [PATCH 027/352] Compiler fix --- src/components/AccountSwitcher.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 428ea27f0fe0..faae7a481d66 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -45,7 +45,7 @@ function AccountSwitcher() { const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false; const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate); - const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(currentUserPersonalDetails?.displayName), [currentUserPersonalDetails]); + const processedTextArray = EmojiUtils.splitTextWithEmojis(currentUserPersonalDetails?.displayName); const createBaseMenuItem = (personalDetails: PersonalDetails | undefined, error?: TranslationPaths, additionalProps: MenuItemWithLink = {}): MenuItemWithLink => { return { From dda7fdf6745d212dad26b8676c456aa1efbd1381 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 30 Aug 2024 11:22:38 +0200 Subject: [PATCH 028/352] Code improvement --- src/pages/home/report/comment/TextCommentFragment.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/report/comment/TextCommentFragment.tsx b/src/pages/home/report/comment/TextCommentFragment.tsx index 7c39ade32e40..703baac97044 100644 --- a/src/pages/home/report/comment/TextCommentFragment.tsx +++ b/src/pages/home/report/comment/TextCommentFragment.tsx @@ -98,7 +98,6 @@ function TextCommentFragment({fragment, styleAsDeleted, styleAsMuted = false, so Date: Tue, 3 Sep 2024 17:27:00 +0200 Subject: [PATCH 029/352] Fix web emoji display --- .../index.native.tsx | 27 +++++++++++++++++++ .../WorkspacesListRowDisplayName/index.tsx | 21 +++++++++++++++ .../WorkspacesListRowDisplayName/types.tsx | 9 +++++++ src/pages/workspace/WorkspacesListRow.tsx | 22 ++++----------- 4 files changed, 62 insertions(+), 17 deletions(-) create mode 100644 src/components/WorkspacesListRowDisplayName/index.native.tsx create mode 100644 src/components/WorkspacesListRowDisplayName/index.tsx create mode 100644 src/components/WorkspacesListRowDisplayName/types.tsx diff --git a/src/components/WorkspacesListRowDisplayName/index.native.tsx b/src/components/WorkspacesListRowDisplayName/index.native.tsx new file mode 100644 index 000000000000..e9db04c18aae --- /dev/null +++ b/src/components/WorkspacesListRowDisplayName/index.native.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type WorkspacesListRowDisplayNameProps from './types'; + +function WorkspacesListRowDisplayName({isDeleted, ownerName}: WorkspacesListRowDisplayNameProps) { + const styles = useThemeStyles(); + const processedOwnerName = EmojiUtils.splitTextWithEmojis(ownerName); + + return ( + + {processedOwnerName.length !== 0 + ? processedOwnerName.map(({text, isEmoji}) => + isEmoji ? {text} : text, + ) + : ownerName} + + ); +} + +WorkspacesListRowDisplayName.displayName = 'WorkspacesListRowDisplayName'; + +export default WorkspacesListRowDisplayName; diff --git a/src/components/WorkspacesListRowDisplayName/index.tsx b/src/components/WorkspacesListRowDisplayName/index.tsx new file mode 100644 index 000000000000..0d3acb736d2f --- /dev/null +++ b/src/components/WorkspacesListRowDisplayName/index.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Text from '@components/Text'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type WorkspacesListRowDisplayNameProps from './types'; + +function WorkspacesListRowDisplayName({isDeleted, ownerName}: WorkspacesListRowDisplayNameProps) { + const styles = useThemeStyles(); + + return ( + + {ownerName} + + ); +} + +WorkspacesListRowDisplayName.displayName = 'WorkspacesListRowDisplayName'; + +export default WorkspacesListRowDisplayName; diff --git a/src/components/WorkspacesListRowDisplayName/types.tsx b/src/components/WorkspacesListRowDisplayName/types.tsx new file mode 100644 index 000000000000..0744ebc18fc1 --- /dev/null +++ b/src/components/WorkspacesListRowDisplayName/types.tsx @@ -0,0 +1,9 @@ +type WorkspacesListRowDisplayNameProps = { + /** Should the deleted style be applied */ + isDeleted: boolean; + + /** Workspace owner name */ + ownerName: string; +}; + +export default WorkspacesListRowDisplayNameProps; diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 5e23e88cb167..6b5d2230c039 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -12,11 +12,11 @@ import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import WorkspacesListRowDisplayName from '@components/WorkspacesListRowDisplayName'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as EmojiUtils from '@libs/EmojiUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import type {AvatarSource} from '@libs/UserUtils'; import type {AnchorPosition} from '@styles/index'; @@ -117,8 +117,6 @@ function WorkspacesListRow({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const ownerDetails = ownerAccountID && PersonalDetailsUtils.getPersonalDetailsByIDs([ownerAccountID], currentUserPersonalDetails.accountID)[0]; - const ownerName = ownerDetails ? PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails) : ''; - const processedOwnerName = EmojiUtils.splitTextWithEmojis(ownerName); const userFriendlyWorkspaceType = useMemo(() => { switch (workspaceType) { @@ -220,20 +218,10 @@ function WorkspacesListRow({ containerStyles={styles.workspaceOwnerAvatarWrapper} /> - - {processedOwnerName.length !== 0 - ? processedOwnerName.map(({text, isEmoji}) => - isEmoji ? ( - {text} - ) : ( - text - ), - ) - : ownerName} - + Date: Wed, 11 Sep 2024 13:14:54 +0200 Subject: [PATCH 030/352] Add missed import --- src/components/AccountSwitcher.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 2d69463f7845..b6f89e097560 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -10,6 +10,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate'; +import * as EmojiUtils from '@libs/EmojiUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import variables from '@styles/variables'; From 528c08f5b91da0d4b91cb770820b929b904cab8f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 12 Sep 2024 10:04:27 +0200 Subject: [PATCH 031/352] Fix regex usage --- src/CONST.ts | 2 +- src/libs/EmojiUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 96374cb2dd1a..173f7d876b98 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2551,7 +2551,7 @@ const CONST = { }, get ALL_EMOJIS() { - return new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g')); + return new RegExp(this.EMOJIS, this.EMOJIS.flags.concat('g')); }, MERGED_ACCOUNT_PREFIX: /^(MERGED_\d+@)/, diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 5ff3ff40e4c1..09c4fd0d0a60 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -604,7 +604,7 @@ function splitTextWithEmojis(text = ''): TextWithEmoji[] { // The regex needs to be cloned because `exec()` is a stateful operation and maintains the state inside // the regex variable itself, so we must have an independent instance for each function's call. - const emojisRegex = CONST.REGEX.ALL_EMOJIS; + const emojisRegex = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g')); const splitText: TextWithEmoji[] = []; let regexResult: RegExpExecArray | null; From eb3a9585a09de11ae11af2569cdf693487bc64db Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 12 Sep 2024 11:09:54 +0200 Subject: [PATCH 032/352] Fix only emojis cropping in the composer on ios --- src/components/Composer/index.native.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/components/Composer/index.native.tsx b/src/components/Composer/index.native.tsx index a95881031683..9b886aa49c1d 100644 --- a/src/components/Composer/index.native.tsx +++ b/src/components/Composer/index.native.tsx @@ -1,7 +1,7 @@ import type {MarkdownStyle} from '@expensify/react-native-live-markdown'; import mimeDb from 'mime-db'; import type {ForwardedRef} from 'react'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {NativeSyntheticEvent, TextInput, TextInputChangeEventData, TextInputPasteEventData} from 'react-native'; import {StyleSheet} from 'react-native'; import type {FileObject} from '@components/AttachmentModal'; @@ -16,7 +16,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullComposerAvailable'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; -import variables from '@styles/variables'; import type {ComposerProps} from './types'; const excludeNoStyles: Array = []; @@ -43,7 +42,6 @@ function Composer( ref: ForwardedRef, ) { const textInput = useRef(null); - const [hasMultipleLines, setHasMultipleLines] = useState(false); const {isFocused, shouldResetFocusRef} = useResetComposerFocus(textInput); const textContainsOnlyEmojis = useMemo(() => EmojiUtils.containsOnlyEmojis(value ?? ''), [value]); const theme = useTheme(); @@ -109,10 +107,7 @@ function Composer( ); const maxHeightStyle = useMemo(() => StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), [StyleUtils, isComposerFullSize, maxLines]); - const composerStyle = useMemo( - () => StyleSheet.flatten([style, textContainsOnlyEmojis && hasMultipleLines ? styles.onlyEmojisTextLineHeight : {}]), - [style, textContainsOnlyEmojis, hasMultipleLines, styles], - ); + const composerStyle = useMemo(() => StyleSheet.flatten([style, textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}]), [style, textContainsOnlyEmojis, styles]); return ( { - setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge); - updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true); - }} + onContentSizeChange={(e) => updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles, true)} rejectResponderTermination={false} smartInsertDelete={false} textAlignVertical="center" From 8768859e04357c3bfd0992c30de0e21c2d7d5605 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Fri, 20 Sep 2024 09:42:33 +0700 Subject: [PATCH 033/352] fix: deleted workspace with invoices is accessible by url --- src/pages/workspace/WorkspaceInitialPage.tsx | 11 ++++++----- .../workspace/WorkspacePageWithSections.tsx | 19 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index fd7a45e31acb..7f51af6192a5 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -94,6 +94,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors)); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); const hasSyncError = PolicyUtils.hasSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy)); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); @@ -306,11 +307,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); + const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); + const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundPage = - isEmptyObject(policy) || - // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace - (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)); + const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); useEffect(() => { if (isEmptyObject(prevPolicy) || PolicyUtils.isPendingDeletePolicy(prevPolicy) || !PolicyUtils.isPendingDeletePolicy(policy)) { @@ -360,7 +361,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyCategories onBackButtonPress={Navigation.dismissModal} onLinkPress={Navigation.resetToHome} shouldShow={shouldShowNotFoundPage} - subtitleKey={isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized'} + subtitleKey={shouldShowPolicy ? 'workspace.common.notAuthorized' : undefined} > fetchData(policyID, shouldSkipVBBACall)}); + const {isOffline} = useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); + const [currentUserLogin] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.email}); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; @@ -148,7 +149,6 @@ function WorkspacePageWithSections({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); - const prevPolicy = usePrevious(policy); useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true @@ -161,19 +161,18 @@ function WorkspacePageWithSections({ }, [policyID, shouldSkipVBBACall]), ); + const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); + const prevShouldShowPolicy = usePrevious(shouldShowPolicy); const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { return true; } - // We check isPendingDelete for both policy and prevPolicy to prevent the NotFound view from showing right after we delete the workspace - return ( - (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || - (PolicyUtils.isPendingDeletePolicy(policy) && PolicyUtils.isPendingDeletePolicy(prevPolicy)) - ); + // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace + return (!isEmptyObject(policy) && !PolicyUtils.isPolicyAdmin(policy) && !shouldShowNonAdmin) || (!shouldShowPolicy && !prevShouldShowPolicy); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [policy, shouldShowNonAdmin]); + }, [policy, shouldShowNonAdmin, shouldShowPolicy, prevShouldShowPolicy]); return ( Date: Mon, 23 Sep 2024 10:53:49 +0200 Subject: [PATCH 034/352] Fix lint check errors --- src/libs/ValidationUtils.ts | 2 +- .../settings/Profile/DisplayNamePage.tsx | 20 +++++-------------- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 10c74865bb51..a1c1b44ccaf7 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -21,7 +21,7 @@ import StringUtils from './StringUtils'; function validateCardNumber(value: string): boolean { let sum = 0; for (let i = 0; i < value.length; i++) { - let intVal = parseInt(value.substr(i, 1), 10); + let intVal = parseInt(value.charAt(i), 10); if (i % 2 === 0) { intVal *= 2; if (intVal > 9) { diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx index 35f5d77cb124..4c6211bc3e37 100644 --- a/src/pages/settings/Profile/DisplayNamePage.tsx +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -1,7 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; @@ -22,11 +21,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/DisplayNameForm'; -type DisplayNamePageOnyxProps = { - isLoadingApp: OnyxEntry; -}; - -type DisplayNamePageProps = DisplayNamePageOnyxProps & WithCurrentUserPersonalDetailsProps; +type DisplayNamePageProps = WithCurrentUserPersonalDetailsProps; /** * Submit form to update user's first and last name (and display name) @@ -36,9 +31,10 @@ const updateDisplayName = (values: FormOnyxValues({ - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, - })(DisplayNamePage), -); +export default withCurrentUserPersonalDetails(DisplayNamePage); From 729b67be7f8316547b563003d4cb5d11b12efb09 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Sat, 28 Sep 2024 01:33:25 +0700 Subject: [PATCH 035/352] use prevPolicy --- src/pages/workspace/WorkspaceInitialPage.tsx | 2 +- src/pages/workspace/WorkspacePageWithSections.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 33330be8d9fb..156282c9f281 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -298,7 +298,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index fec440da970a..cf473ebec0ba 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -139,6 +139,7 @@ function WorkspacePageWithSections({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); + const prevPolicy = usePrevious(policy); useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true @@ -152,7 +153,7 @@ function WorkspacePageWithSections({ ); const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = usePrevious(shouldShowPolicy); + const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { From 093f361d0cee939407b5872c889ddc565ff85881 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Sat, 28 Sep 2024 01:33:41 +0700 Subject: [PATCH 036/352] remove redundant changes --- src/pages/workspace/WorkspacePageWithSections.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index cf473ebec0ba..26175c9793d9 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -140,7 +140,6 @@ function WorkspacePageWithSections({ const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); const prevPolicy = usePrevious(policy); - useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true firstRender.current = false; From 70190a45ee96f2f9fa75c74e492a3ec1d84d7512 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 1 Oct 2024 10:45:48 +0200 Subject: [PATCH 037/352] Minor fix --- src/libs/EmojiUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 09c4fd0d0a60..9b1f1806e94f 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -596,7 +596,7 @@ function splitTextWithEmojis(text = ''): TextWithEmoji[] { return []; } - const doesTextContainEmojis = CONST.REGEX.ALL_EMOJIS.test(text); + const doesTextContainEmojis = new RegExp(CONST.REGEX.EMOJIS, CONST.REGEX.EMOJIS.flags.concat('g')).test(text); if (!doesTextContainEmojis) { return []; From ac0f5b90eda70154b6612310766f1cc394678458 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 2 Oct 2024 05:55:42 +0530 Subject: [PATCH 038/352] =?UTF-8?q?feat:=20Implement=20to=20use=20a=20?= =?UTF-8?q?=F0=9F=91=8Dicon=20next=20to=20approved=20report=20preview.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 87f06f43d82a..94755ebb6944 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -145,12 +145,18 @@ function ReportPreview({ transform: [{scale: checkMarkScale.value}], })); + const isApproved = ReportUtils.isReportApproved(iouReport, action); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); + const thumbsUpStyle = useAnimatedStyle(() => ({ + ...styles.defaultCheckmarkWrapper, + transform: [{scale: thumbsUpScale.value}], + })); + const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const isInvoiceRoom = ReportUtils.isInvoiceRoom(chatReport); const isOpenExpenseReport = isPolicyExpenseChat && ReportUtils.isOpenExpenseReport(iouReport); - const isApproved = ReportUtils.isReportApproved(iouReport, action); const canAllowSettlement = ReportUtils.hasUpdatedTotal(iouReport, policy); const numberOfRequests = allTransactions.length; const transactionsWithReceipts = ReportUtils.getTransactionsWithReceipts(iouReportID); @@ -433,6 +439,14 @@ function ReportPreview({ } }, [isPaidAnimationRunning, iouSettled, checkMarkScale]); + useEffect(() => { + if (!isApproved) { + return; + } + + thumbsUpScale.value = withSpring(1, {duration: 200}); + }, [isApproved, thumbsUpScale]); + return ( )} + {isApproved && ( + + + + )} {shouldShowSubtitle && supportText && ( From bce816dc449b2c94aa14bc96514998c38fdbb29c Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 4 Oct 2024 09:31:15 +0200 Subject: [PATCH 039/352] TS fix --- .../HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx index fd9152092c05..879684210825 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/EmojiRenderer.tsx @@ -1,4 +1,5 @@ import React, {useMemo} from 'react'; +import type {TextStyle} from 'react-native'; import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html'; import EmojiWithTooltip from '@components/EmojiWithTooltip'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -7,15 +8,15 @@ function EmojiRenderer({tnode, style: styleProp}: CustomRendererProps { if ('islarge' in tnode.attributes) { - return [styleProp, styles.onlyEmojisText]; + return [styleProp as TextStyle, styles.onlyEmojisText]; } if ('ismedium' in tnode.attributes) { - return [styleProp, styles.emojisWithTextFontSize, styles.verticalAlignTopText]; + return [styleProp as TextStyle, styles.emojisWithTextFontSize, styles.verticalAlignTopText]; } return null; - }, [tnode.attributes, styles]); + }, [tnode.attributes, styles, styleProp]); return ( Date: Fri, 4 Oct 2024 09:40:52 +0200 Subject: [PATCH 040/352] Update react-native-live-markdown version --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf9978bae510..87e6d11c5829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.143", + "@expensify/react-native-live-markdown": "0.1.163", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -3634,9 +3634,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.143", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.143.tgz", - "integrity": "sha512-hZXYjKyTl/b2p7Ig9qhoB7cfVtTTcoE2cWvea8NJT3f5ZYckdyHDAgHI4pg0S0N68jP205Sk5pzqlltZUpZk5w==", + "version": "0.1.163", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.163.tgz", + "integrity": "sha512-kIVWxKPHbXrLJ28TmI4sOndFjo8LktoytSiea4tS/GhVQXkbzZdtyTzNVfrQPSsJq+ITUbg701aC5XNqDhIAnQ==", "workspaces": [ "parser", "example", diff --git a/package.json b/package.json index baf05e92111b..07ddc3ad6371 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.143", + "@expensify/react-native-live-markdown": "0.1.163", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", From cde40ee4ab1c32eac4d2b2187807a4f876de502f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 4 Oct 2024 10:01:19 +0200 Subject: [PATCH 041/352] Revert "Update react-native-live-markdown version" This reverts commit 9a4883bc82242ce848acfaea976d99ce7f32660c. --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87e6d11c5829..cf9978bae510 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "MIT", "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.163", + "@expensify/react-native-live-markdown": "0.1.143", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", @@ -3634,9 +3634,9 @@ } }, "node_modules/@expensify/react-native-live-markdown": { - "version": "0.1.163", - "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.163.tgz", - "integrity": "sha512-kIVWxKPHbXrLJ28TmI4sOndFjo8LktoytSiea4tS/GhVQXkbzZdtyTzNVfrQPSsJq+ITUbg701aC5XNqDhIAnQ==", + "version": "0.1.143", + "resolved": "https://registry.npmjs.org/@expensify/react-native-live-markdown/-/react-native-live-markdown-0.1.143.tgz", + "integrity": "sha512-hZXYjKyTl/b2p7Ig9qhoB7cfVtTTcoE2cWvea8NJT3f5ZYckdyHDAgHI4pg0S0N68jP205Sk5pzqlltZUpZk5w==", "workspaces": [ "parser", "example", diff --git a/package.json b/package.json index 07ddc3ad6371..baf05e92111b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ }, "dependencies": { "@dotlottie/react-player": "^1.6.3", - "@expensify/react-native-live-markdown": "0.1.163", + "@expensify/react-native-live-markdown": "0.1.143", "@expo/metro-runtime": "~3.2.3", "@firebase/app": "^0.10.10", "@firebase/performance": "^0.6.8", From beb006f9e68dfc560e39183a219a337937f7e7d8 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 16:35:53 +0530 Subject: [PATCH 042/352] fix: Room - Create room whisper reappears when interacting with it after workspace is deleted. Signed-off-by: krishna2323 --- .../AttachmentCarousel/extractAttachments.ts | 5 +++-- .../AttachmentCarousel/index.native.tsx | 4 ++-- .../Attachments/AttachmentCarousel/index.tsx | 18 +++++++++++++++--- src/components/ParentNavigationSubtitle.tsx | 2 +- src/libs/OptionsListUtils.ts | 4 ++-- src/libs/ReportActionsUtils.ts | 19 ++++++++++++++----- src/libs/SidebarUtils.ts | 2 +- src/pages/home/ReportScreen.tsx | 5 ++++- src/pages/home/report/ReportActionsList.tsx | 4 ++-- 9 files changed, 44 insertions(+), 19 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 81ee6d08934b..69b0b8229f4a 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -19,7 +19,8 @@ function extractAttachments( accountID, parentReportAction, reportActions, - }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry}, + reportID, + }: {privateNotes?: Record; accountID?: number; parentReportAction?: OnyxEntry; reportActions?: OnyxEntry; reportID: string}, ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; @@ -95,7 +96,7 @@ function extractAttachments( const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; actions.forEach((action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key) || ReportActionsUtils.isMoneyRequestAction(action)) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID) || ReportActionsUtils.isMoneyRequestAction(action)) { return; } diff --git a/src/components/Attachments/AttachmentCarousel/index.native.tsx b/src/components/Attachments/AttachmentCarousel/index.native.tsx index a8eb614202a7..9aa619eb1cda 100644 --- a/src/components/Attachments/AttachmentCarousel/index.native.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.native.tsx @@ -34,9 +34,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions, reportID: report.reportID}); } let newIndex = newAttachments.findIndex(compareImage); diff --git a/src/components/Attachments/AttachmentCarousel/index.tsx b/src/components/Attachments/AttachmentCarousel/index.tsx index a1408aaf400e..ac4975d85665 100644 --- a/src/components/Attachments/AttachmentCarousel/index.tsx +++ b/src/components/Attachments/AttachmentCarousel/index.tsx @@ -76,9 +76,9 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi const parentReportAction = report.parentReportActionID && parentReportActions ? parentReportActions[report.parentReportActionID] : undefined; let newAttachments: Attachment[] = []; if (type === CONST.ATTACHMENT_TYPE.NOTE && accountID) { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.NOTE, {privateNotes: report.privateNotes, accountID, reportID: report.reportID}); } else { - newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined}); + newAttachments = extractAttachments(CONST.ATTACHMENT_TYPE.REPORT, {parentReportAction, reportActions: reportActions ?? undefined, reportID: report.reportID}); } if (isEqual(attachments, newAttachments)) { @@ -117,7 +117,19 @@ function AttachmentCarousel({report, source, onNavigate, setDownloadButtonVisibi onNavigate(attachment); } } - }, [report.privateNotes, reportActions, parentReportActions, compareImage, report.parentReportActionID, attachments, setDownloadButtonVisibility, onNavigate, accountID, type]); + }, [ + report.privateNotes, + reportActions, + parentReportActions, + compareImage, + report.parentReportActionID, + attachments, + setDownloadButtonVisibility, + onNavigate, + accountID, + type, + report.reportID, + ]); // Scroll position is affected when window width is resized, so we readjust it on width changes useEffect(() => { diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 997106f3e649..ef0f981a8c77 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -39,7 +39,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct { const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1'); - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1'); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', parentReportID); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(parentReportID)); if (isVisibleAction && !isOffline) { diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fbf2f3b94c7c..2e19d5a9538f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -327,7 +327,7 @@ Onyx.connect({ // does not match a closed or created state. const reportActionsForDisplay = sortedReportActions.filter( (reportAction, actionKey) => - ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, reportID) && !ReportActionUtils.isWhisperAction(reportAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && @@ -677,7 +677,7 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails const iouReport = ReportUtils.getReportOrDraftReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction)); const lastIOUMoneyReportAction = allSortedReportActions[iouReport?.reportID ?? '-1']?.find( (reportAction, key): reportAction is ReportAction => - ReportActionUtils.shouldReportActionBeVisible(reportAction, key) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, key, reportID) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3b5e0a8eeaa3..105fbee48c31 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -27,6 +27,7 @@ import Parser from './Parser'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; +import * as ReportUtils from './ReportUtils'; import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle @@ -630,7 +631,15 @@ const supportedActionTypes: ReportActionName[] = [...Object.values(otherActionTy * Checks if a reportAction is fit for display, meaning that it's not deprecated, is of a valid * and supported type, it's not deleted and also not closed. */ -function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number): boolean { +function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, reportID: string): boolean { + const report = ReportUtils.getReport(reportID); + if ( + (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(report?.reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && + !ReportUtils.canUserPerformWriteAction(report) + ) { + return false; + } + if (!reportAction) { return false; } @@ -706,7 +715,7 @@ function isResolvedActionTrackExpense(reportAction: OnyxEntry): bo * Checks if a reportAction is fit for display as report last action, meaning that * it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted. */ -function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry): boolean { +function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry, reportID: string): boolean { if (!reportAction) { return false; } @@ -718,7 +727,7 @@ function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry< // If a whisper action is the REPORT_PREVIEW action, we are displaying it. // If the action's message text is empty and it is not a deleted parent with visible child actions, hide it. Else, consider the action to be displayable. return ( - shouldReportActionBeVisible(reportAction, reportAction.reportActionID) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, reportID) && !(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) && !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction)) && !isResolvedActionTrackExpense(reportAction) @@ -760,7 +769,7 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record shouldReportActionBeVisibleAsLastAction(action)); + const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, reportID)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -1087,7 +1096,7 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn */ function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean { const reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge, true)); - const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action)); + const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action, reportID)); // Exclude the task system message and the created message const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action)); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index eb5b3c58cdef..4c2a245af9dc 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -46,7 +46,7 @@ Onyx.connect({ // The report is only visible if it is the last action not deleted that // does not match a closed or created state. const reportActionsForDisplay = actionsArray.filter( - (reportAction) => ReportActionsUtils.shouldReportActionBeVisibleAsLastAction(reportAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, + (reportAction) => ReportActionsUtils.shouldReportActionBeVisibleAsLastAction(reportAction, reportID) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, ); const reportAction = reportActionsForDisplay.at(-1); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 8afeb0cf2307..56545e80aeae 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -335,7 +335,10 @@ function ReportScreen({route, currentReportID = '', navigation}: ReportScreenPro ? reportActions.length > 0 : reportActions.length >= CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0); - const isLinkedActionDeleted = useMemo(() => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID), [linkedAction]); + const isLinkedActionDeleted = useMemo( + () => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, report?.reportID ?? '-1'), + [linkedAction, report?.reportID], + ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); const isLinkedActionInaccessibleWhisper = useMemo( () => !!linkedAction && ReportActionsUtils.isWhisperAction(linkedAction) && !(linkedAction?.whisperedToAccountIDs ?? []).includes(currentUserAccountID), diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index ce925d4375af..6025fe64ffb2 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -193,9 +193,9 @@ function ReportActionsList({ ReportActionsUtils.isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID), + ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, report.reportID), ), - [sortedReportActions, isOffline], + [sortedReportActions, isOffline, report.reportID], ); /** From 921f47429960cd6b5f1110e50f5bb7139a6736e2 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 16:43:39 +0530 Subject: [PATCH 043/352] fix lint issues. Signed-off-by: krishna2323 --- src/libs/ReportActionsUtils.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 105fbee48c31..8681980163e6 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -28,7 +28,6 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; import * as ReportUtils from './ReportUtils'; -import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; @@ -123,7 +122,7 @@ function isCreatedAction(reportAction: OnyxInputOrEntry): boolean return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; } -function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { +function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { const message = reportAction?.message ?? []; if (!Array.isArray(message)) { @@ -136,7 +135,7 @@ function isDeletedAction(reportAction: OnyxInputOrEntry): bo return (getReportActionMessage(reportAction)?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isReversedTransaction(reportAction: OnyxInputOrEntry) { +function isReversedTransaction(reportAction: OnyxInputOrEntry) { return (getReportActionMessage(reportAction)?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0; } @@ -360,7 +359,7 @@ function getParentReportAction(report: OnyxInputOrEntry): OnyxEntry): boolean { +function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { return ( isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && @@ -990,11 +989,11 @@ function isSplitBillAction(reportAction: OnyxInputOrEntry): report return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; } -function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { +function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK; } -function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { +function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY; } @@ -1204,7 +1203,7 @@ function getMemberChangeMessageElements(reportAction: OnyxEntry): ]; } -function getReportActionHtml(reportAction: PartialReportAction): string { +function getReportActionHtml(reportAction: ReportUtils.PartialReportAction): string { return getReportActionMessage(reportAction)?.html ?? ''; } @@ -1220,7 +1219,7 @@ function getTextFromHtml(html?: string): string { return html ? Parser.htmlToText(html) : ''; } -function isOldDotLegacyAction(action: OldDotReportAction | PartialReportAction): action is PartialReportAction { +function isOldDotLegacyAction(action: OldDotReportAction | ReportUtils.PartialReportAction): action is ReportUtils.PartialReportAction { return [ CONST.REPORT.ACTIONS.TYPE.DELETED_ACCOUNT, CONST.REPORT.ACTIONS.TYPE.DONATION, @@ -1260,7 +1259,7 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) { ].some((oldDotActionName) => oldDotActionName === action.actionName); } -function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { +function getMessageOfOldDotLegacyAction(legacyAction: ReportUtils.PartialReportAction) { if (!Array.isArray(legacyAction?.message)) { return getReportActionText(legacyAction); } @@ -1275,7 +1274,7 @@ function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { /** * Helper method to format message of OldDot Actions. */ -function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldDotReportAction, withMarkdown = true): string { +function getMessageOfOldDotReportAction(oldDotAction: ReportUtils.PartialReportAction | OldDotReportAction, withMarkdown = true): string { if (isOldDotLegacyAction(oldDotAction)) { return getMessageOfOldDotLegacyAction(oldDotAction); } From 597e24318607e9d089961b35b9d008ceef0c9894 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 17:58:44 +0530 Subject: [PATCH 044/352] minor update. Signed-off-by: krishna2323 --- .../LHNOptionsList/LHNOptionsList.tsx | 2 +- src/hooks/usePaginatedReportActions.ts | 2 +- src/libs/Middleware/Pagination.ts | 4 +-- src/libs/ReportActionsUtils.ts | 31 ++++++++++--------- src/libs/actions/Report.ts | 2 +- src/pages/Debug/Report/DebugReportActions.tsx | 2 +- .../report/ReportActionItemParentAction.tsx | 6 +++- src/pages/home/report/ReportActionsView.tsx | 2 +- src/pages/home/report/ThreadDivider.tsx | 2 +- .../perf-test/ReportActionsUtils.perf-test.ts | 4 +-- tests/unit/ReportActionsUtilsTest.ts | 6 ++-- 11 files changed, 34 insertions(+), 29 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 08240a211804..b317d3020e2a 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -139,7 +139,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio : '-1'; const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, reportID); const lastReportAction = sortedReportActions.at(0); // Get the transaction for the last report action diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index a32d4f7d3dd0..e3525dcf91f5 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -14,7 +14,7 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) { const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), }); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`); diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index bfa8183ac03b..7b3abdee849c 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -15,7 +15,7 @@ type PagedResource = OnyxValues[TResourc type PaginationCommonConfig = { resourceCollectionKey: TResourceKey; pageCollectionKey: TPageKey; - sortItems: (items: OnyxValues[TResourceKey]) => Array>; + sortItems: (items: OnyxValues[TResourceKey], reportID: string) => Array>; getItemID: (item: PagedResource) => string; }; @@ -96,7 +96,7 @@ const Pagination: Middleware = (requestResponse, request) => { // Create a new page based on the response const pageItems = (response.onyxData.find((data) => data.key === resourceKey)?.value ?? {}) as OnyxValues[typeof resourceCollectionKey]; - const sortedPageItems = sortItems(pageItems); + const sortedPageItems = sortItems(pageItems, resourceID); if (sortedPageItems.length === 0) { // Must have at least 1 action to create a page. Log.hmmm(`[Pagination] Did not receive any items in the response to ${request.command}`); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8681980163e6..8a1ff9431e28 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -27,7 +27,8 @@ import Parser from './Parser'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; -import * as ReportUtils from './ReportUtils'; +import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; +import {canUserPerformWriteAction, getReport} from './ReportUtils.ts'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; @@ -122,7 +123,7 @@ function isCreatedAction(reportAction: OnyxInputOrEntry): boolean return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; } -function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { +function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { const message = reportAction?.message ?? []; if (!Array.isArray(message)) { @@ -135,7 +136,7 @@ function isDeletedAction(reportAction: OnyxInputOrEntry): bo return (getReportActionMessage(reportAction)?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0; } -function isReversedTransaction(reportAction: OnyxInputOrEntry) { +function isReversedTransaction(reportAction: OnyxInputOrEntry) { return (getReportActionMessage(reportAction)?.isReversedTransaction ?? false) && ((reportAction as ReportAction)?.childVisibleActionCount ?? 0) > 0; } @@ -359,7 +360,7 @@ function getParentReportAction(report: OnyxInputOrEntry): OnyxEntry): boolean { +function isSentMoneyReportAction(reportAction: OnyxEntry): boolean { return ( isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && @@ -631,10 +632,10 @@ const supportedActionTypes: ReportActionName[] = [...Object.values(otherActionTy * and supported type, it's not deleted and also not closed. */ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, reportID: string): boolean { - const report = ReportUtils.getReport(reportID); + const report = getReport(reportID); if ( (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(report?.reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && - !ReportUtils.canUserPerformWriteAction(report) + !canUserPerformWriteAction(report) ) { return false; } @@ -834,7 +835,7 @@ function filterOutDeprecatedReportActions(reportActions: OnyxEntry | ReportAction[], shouldIncludeInvisibleActions = false): ReportAction[] { +function getSortedReportActionsForDisplay(reportActions: OnyxEntry | ReportAction[], reportID: string, shouldIncludeInvisibleActions = false): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { return []; @@ -844,7 +845,7 @@ function getSortedReportActionsForDisplay(reportActions: OnyxEntry shouldReportActionBeVisible(reportAction, key)) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, reportID)) .map(([, reportAction]) => reportAction); } @@ -989,11 +990,11 @@ function isSplitBillAction(reportAction: OnyxInputOrEntry): report return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT; } -function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { +function isTrackExpenseAction(reportAction: OnyxEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.TRACK; } -function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { +function isPayAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU) && getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY; } @@ -1203,7 +1204,7 @@ function getMemberChangeMessageElements(reportAction: OnyxEntry): ]; } -function getReportActionHtml(reportAction: ReportUtils.PartialReportAction): string { +function getReportActionHtml(reportAction: PartialReportAction): string { return getReportActionMessage(reportAction)?.html ?? ''; } @@ -1219,7 +1220,7 @@ function getTextFromHtml(html?: string): string { return html ? Parser.htmlToText(html) : ''; } -function isOldDotLegacyAction(action: OldDotReportAction | ReportUtils.PartialReportAction): action is ReportUtils.PartialReportAction { +function isOldDotLegacyAction(action: OldDotReportAction | PartialReportAction): action is PartialReportAction { return [ CONST.REPORT.ACTIONS.TYPE.DELETED_ACCOUNT, CONST.REPORT.ACTIONS.TYPE.DONATION, @@ -1259,7 +1260,7 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) { ].some((oldDotActionName) => oldDotActionName === action.actionName); } -function getMessageOfOldDotLegacyAction(legacyAction: ReportUtils.PartialReportAction) { +function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { if (!Array.isArray(legacyAction?.message)) { return getReportActionText(legacyAction); } @@ -1274,7 +1275,7 @@ function getMessageOfOldDotLegacyAction(legacyAction: ReportUtils.PartialReportA /** * Helper method to format message of OldDot Actions. */ -function getMessageOfOldDotReportAction(oldDotAction: ReportUtils.PartialReportAction | OldDotReportAction, withMarkdown = true): string { +function getMessageOfOldDotReportAction(oldDotAction: PartialReportAction | OldDotReportAction, withMarkdown = true): string { if (isOldDotLegacyAction(oldDotAction)) { return getMessageOfOldDotLegacyAction(oldDotAction); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 13b14d380758..7ac929b9fad0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -274,7 +274,7 @@ registerPaginationConfig({ nextCommand: READ_COMMANDS.GET_NEWER_ACTIONS, resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - sortItems: (reportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + sortItems: (reportActions, reportID) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportID, true), getItemID: (reportAction) => reportAction.reportActionID, }); diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index e7c4059fffe7..d25c9175a4b3 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -23,7 +23,7 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { const styles = useThemeStyles(); const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), }); const renderItem = ({item}: ListRenderItemInfo) => ( { - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1'); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( + ancestor.reportAction, + ancestor.reportAction.reportActionID ?? '-1', + ancestor.report.reportID, + ); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1')); if (isVisibleAction && !isOffline) { diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 8896611905ca..b58de22bc520 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -90,7 +90,7 @@ function ReportActionsView({ const route = useRoute>(); const [session] = useOnyx(ONYXKEYS.SESSION); const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? -1}`, { - selector: (reportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true), + selector: (reportActions: OnyxEntry) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, report.reportID, true), }); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? -1}`); const prevTransactionThreadReport = usePrevious(transactionThreadReport); diff --git a/src/pages/home/report/ThreadDivider.tsx b/src/pages/home/report/ThreadDivider.tsx index d2ffa97f58b2..6fec617e4c37 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -47,7 +47,7 @@ function ThreadDivider({ancestor, isLinkDisabled = false}: ThreadDividerProps) { ) : ( { - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1'); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1', ancestor.report.reportID); // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1')); if (isVisibleAction && !isOffline) { diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts index a33a448cfee7..5e258436edc7 100644 --- a/tests/perf-test/ReportActionsUtils.perf-test.ts +++ b/tests/perf-test/ReportActionsUtils.perf-test.ts @@ -93,7 +93,7 @@ describe('ReportActionsUtils', () => { }); test('[ReportActionsUtils] getMostRecentIOURequestActionID on 10k ReportActions', async () => { - const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); + const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId); await waitForBatchedUpdates(); await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray)); @@ -132,7 +132,7 @@ describe('ReportActionsUtils', () => { test('[ReportActionsUtils] getSortedReportActionsForDisplay on 10k ReportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions)); + await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId)); }); test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index d753069265f8..b6e29e89d025 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -302,7 +302,7 @@ describe('ReportActionsUtils', () => { }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); expect(result).toStrictEqual(input); }); @@ -392,7 +392,7 @@ describe('ReportActionsUtils', () => { ], }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); input.pop(); expect(result).toStrictEqual(input); }); @@ -437,7 +437,7 @@ describe('ReportActionsUtils', () => { message: [{html: '', type: 'Action type', text: 'Action text'}], }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); input.pop(); expect(result).toStrictEqual(input); }); From 352abf82c0e2c577133871a18813af3189dbe22b Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 17:59:42 +0530 Subject: [PATCH 045/352] minor update. Signed-off-by: krishna2323 --- src/libs/Middleware/Pagination.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Middleware/Pagination.ts b/src/libs/Middleware/Pagination.ts index 7b3abdee849c..251609d1254c 100644 --- a/src/libs/Middleware/Pagination.ts +++ b/src/libs/Middleware/Pagination.ts @@ -115,7 +115,7 @@ const Pagination: Middleware = (requestResponse, request) => { const resourceCollections = resources.get(resourceCollectionKey) ?? {}; const existingItems = resourceCollections[resourceKey] ?? {}; const allItems = fastMerge(existingItems, pageItems, true); - const sortedAllItems = sortItems(allItems); + const sortedAllItems = sortItems(allItems, resourceID); const pagesCollections = pages.get(pageCollectionKey) ?? {}; const existingPages = pagesCollections[pageKey] ?? []; From 592dc56b262f3cb85098d6a30cb9da7015f125b4 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Sun, 13 Oct 2024 14:41:40 +0200 Subject: [PATCH 046/352] Add keys to get rid of lint errors --- src/components/AccountSwitcher.tsx | 14 +++++++++++++- .../TextWithTooltip/index.native.tsx | 14 +++++++++++++- .../index.native.tsx | 14 ++++++++++++-- .../index.native.tsx | 14 +++++++++++++- .../index.tsx | 16 +++++++++++++++- .../TextWithEmojiFragment/index.native.tsx | 7 +++++-- .../comment/TextWithEmojiFragment/index.tsx | 18 +++++++++++++++++- 7 files changed, 88 insertions(+), 9 deletions(-) diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 207f77b7ee24..12932c58bbc3 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -153,7 +153,19 @@ function AccountSwitcher() { style={[styles.textBold, styles.textLarge, styles.flexShrink1]} > {processedTextArray.length !== 0 - ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) + ? processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + text + ), + ) : currentUserPersonalDetails?.displayName} {canSwitchAccounts && ( diff --git a/src/components/TextWithTooltip/index.native.tsx b/src/components/TextWithTooltip/index.native.tsx index 892b34649d93..7084156ec7de 100644 --- a/src/components/TextWithTooltip/index.native.tsx +++ b/src/components/TextWithTooltip/index.native.tsx @@ -14,7 +14,19 @@ function TextWithTooltip({text, style, numberOfLines = 1}: TextWithTooltipProps) numberOfLines={numberOfLines} > {processedTextArray.length !== 0 - ? processedTextArray.map(({text: textItem, isEmoji}) => (isEmoji ? {textItem} : textItem)) + ? processedTextArray.map(({text: textItem, isEmoji}, index) => + isEmoji ? ( + + {textItem} + + ) : ( + textItem + ), + ) : text} ); diff --git a/src/components/WorkspacesListRowDisplayName/index.native.tsx b/src/components/WorkspacesListRowDisplayName/index.native.tsx index e9db04c18aae..a5d017cc69a7 100644 --- a/src/components/WorkspacesListRowDisplayName/index.native.tsx +++ b/src/components/WorkspacesListRowDisplayName/index.native.tsx @@ -14,8 +14,18 @@ function WorkspacesListRowDisplayName({isDeleted, ownerName}: WorkspacesListRowD style={[styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}]} > {processedOwnerName.length !== 0 - ? processedOwnerName.map(({text, isEmoji}) => - isEmoji ? {text} : text, + ? processedOwnerName.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + text + ), ) : ownerName} diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx index f4fa8285eb42..849174f6c9a0 100644 --- a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx @@ -20,7 +20,19 @@ function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateA style={[styles.chatItemMessageHeaderSender, isSingleLine ? styles.pre : styles.preWrap, styles.dFlex]} > {processedTextArray.length !== 0 - ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) + ? processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + text + ), + ) : fragmentText}
diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx index d2bedbd3f18b..869c550647ee 100644 --- a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx @@ -19,7 +19,21 @@ function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateA numberOfLines={isSingleLine ? 1 : undefined} style={[styles.chatItemMessageHeaderSender, isSingleLine ? styles.pre : styles.preWrap]} > - {processedTextArray.length !== 0 ? processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : text)) : fragmentText} + {processedTextArray.length !== 0 + ? processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + text + ), + ) + : fragmentText} ); diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx index f4efeefc6623..538ca4e9deb6 100644 --- a/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.native.tsx @@ -12,9 +12,12 @@ function TextWithEmojiFragment({message = '', style}: TextWithEmojiFragmentProps return ( - {processedTextArray.map(({text, isEmoji}) => + {processedTextArray.map(({text, isEmoji}, index) => isEmoji ? ( - + {text} ) : ( diff --git a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx index e21a53451f2b..d19725da766d 100644 --- a/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx +++ b/src/pages/home/report/comment/TextWithEmojiFragment/index.tsx @@ -9,7 +9,23 @@ function TextWithEmojiFragment({message = '', style}: TextWithEmojiFragmentProps const styles = useThemeStyles(); const processedTextArray = useMemo(() => EmojiUtils.splitTextWithEmojis(message), [message]); - return {processedTextArray.map(({text, isEmoji}) => (isEmoji ? {text} : convertToLTR(text)))}; + return ( + + {processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + convertToLTR(text) + ), + )} + + ); } TextWithEmojiFragment.displayName = 'TextWithEmojiFragment'; From 8010e02b37e62020480af5e109ab25e3bec5c8cc Mon Sep 17 00:00:00 2001 From: VickyStash Date: Sun, 13 Oct 2024 15:01:35 +0200 Subject: [PATCH 047/352] Clean up code duplicates --- src/components/AccountSwitcher.tsx | 14 +------------ .../TextWithTooltip/index.native.tsx | 16 +-------------- .../index.native.tsx | 14 +------------ src/libs/{EmojiUtils.ts => EmojiUtils.tsx} | 20 +++++++++++++++++++ .../index.native.tsx | 16 +-------------- .../index.tsx | 16 +-------------- 6 files changed, 25 insertions(+), 71 deletions(-) rename src/libs/{EmojiUtils.ts => EmojiUtils.tsx} (97%) diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx index 12932c58bbc3..bbd7bb6a58b8 100644 --- a/src/components/AccountSwitcher.tsx +++ b/src/components/AccountSwitcher.tsx @@ -153,19 +153,7 @@ function AccountSwitcher() { style={[styles.textBold, styles.textLarge, styles.flexShrink1]} > {processedTextArray.length !== 0 - ? processedTextArray.map(({text, isEmoji}, index) => - isEmoji ? ( - - {text} - - ) : ( - text - ), - ) + ? EmojiUtils.getProcessedText(processedTextArray, styles.initialSettingsUsernameEmoji) : currentUserPersonalDetails?.displayName} {canSwitchAccounts && ( diff --git a/src/components/TextWithTooltip/index.native.tsx b/src/components/TextWithTooltip/index.native.tsx index 7084156ec7de..9f5f246ff9d3 100644 --- a/src/components/TextWithTooltip/index.native.tsx +++ b/src/components/TextWithTooltip/index.native.tsx @@ -13,21 +13,7 @@ function TextWithTooltip({text, style, numberOfLines = 1}: TextWithTooltipProps) style={style} numberOfLines={numberOfLines} > - {processedTextArray.length !== 0 - ? processedTextArray.map(({text: textItem, isEmoji}, index) => - isEmoji ? ( - - {textItem} - - ) : ( - textItem - ), - ) - : text} + {processedTextArray.length !== 0 ? EmojiUtils.getProcessedText(processedTextArray, [style, styles.emojisFontFamily]) : text} ); } diff --git a/src/components/WorkspacesListRowDisplayName/index.native.tsx b/src/components/WorkspacesListRowDisplayName/index.native.tsx index a5d017cc69a7..1a91e2857db3 100644 --- a/src/components/WorkspacesListRowDisplayName/index.native.tsx +++ b/src/components/WorkspacesListRowDisplayName/index.native.tsx @@ -14,19 +14,7 @@ function WorkspacesListRowDisplayName({isDeleted, ownerName}: WorkspacesListRowD style={[styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}]} > {processedOwnerName.length !== 0 - ? processedOwnerName.map(({text, isEmoji}, index) => - isEmoji ? ( - - {text} - - ) : ( - text - ), - ) + ? EmojiUtils.getProcessedText(processedOwnerName, [styles.labelStrong, isDeleted ? styles.offlineFeedback.deleted : {}, styles.emojisWithTextFontFamily]) : ownerName} ); diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.tsx similarity index 97% rename from src/libs/EmojiUtils.ts rename to src/libs/EmojiUtils.tsx index 4aa777998d2a..9974fdb30477 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.tsx @@ -1,8 +1,11 @@ import {Str} from 'expensify-common'; +import React from 'react'; +import type {StyleProp, TextStyle} from 'react-native'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import * as Emojis from '@assets/emojis'; import type {Emoji, HeaderEmoji, PickerEmojis} from '@assets/emojis/types'; +import Text from '@components/Text'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx'; @@ -652,6 +655,22 @@ function splitTextWithEmojis(text = ''): TextWithEmoji[] { return splitText; } +function getProcessedText(processedTextArray: TextWithEmoji[], style: StyleProp): Array { + return processedTextArray.map(({text, isEmoji}, index) => + isEmoji ? ( + + {text} + + ) : ( + text + ), + ); +} + export type {HeaderIndice, EmojiPickerList, EmojiSpacer, EmojiPickerListItem}; export { @@ -659,6 +678,7 @@ export { findEmojiByCode, getEmojiName, getLocalizedEmojiName, + getProcessedText, getHeaderEmojis, mergeEmojisWithFrequentlyUsedEmojis, containsOnlyEmojis, diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx index 849174f6c9a0..9a752c3a9007 100644 --- a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.native.tsx @@ -19,21 +19,7 @@ function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateA numberOfLines={isSingleLine ? 1 : undefined} style={[styles.chatItemMessageHeaderSender, isSingleLine ? styles.pre : styles.preWrap, styles.dFlex]} > - {processedTextArray.length !== 0 - ? processedTextArray.map(({text, isEmoji}, index) => - isEmoji ? ( - - {text} - - ) : ( - text - ), - ) - : fragmentText} + {processedTextArray.length !== 0 ? EmojiUtils.getProcessedText(processedTextArray, [styles.emojisWithTextFontSize, styles.emojisWithTextFontFamily]) : fragmentText} ); diff --git a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx index 869c550647ee..d5602dbedfae 100644 --- a/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx +++ b/src/pages/home/report/ReportActionItemMessageHeaderSender/index.tsx @@ -19,21 +19,7 @@ function ReportActionItemMessageHeaderSender({fragmentText, accountID, delegateA numberOfLines={isSingleLine ? 1 : undefined} style={[styles.chatItemMessageHeaderSender, isSingleLine ? styles.pre : styles.preWrap]} > - {processedTextArray.length !== 0 - ? processedTextArray.map(({text, isEmoji}, index) => - isEmoji ? ( - - {text} - - ) : ( - text - ), - ) - : fragmentText} + {processedTextArray.length !== 0 ? EmojiUtils.getProcessedText(processedTextArray, styles.emojisWithTextFontSize) : fragmentText} ); From 534c19e7e5fbd9c934bd725ef5570c9525012530 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 13 Oct 2024 21:44:24 +0530 Subject: [PATCH 048/352] fix lint issue. Signed-off-by: krishna2323 --- src/hooks/usePaginatedReportActions.ts | 2 +- src/libs/ReportActionsUtils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index e3525dcf91f5..342d73b08bd8 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -14,7 +14,7 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) { const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID ?? '-1', true), }); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8a1ff9431e28..0807f8f95ed7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -28,7 +28,7 @@ import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportConnection from './ReportConnection'; import type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; -import {canUserPerformWriteAction, getReport} from './ReportUtils.ts'; +import {canUserPerformWriteAction, getReport} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; From 371c92a8f21b5709b9006e8762d0487f3e94f69f Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Mon, 14 Oct 2024 21:47:12 +0100 Subject: [PATCH 049/352] feat(debug mode): add debug transaction page --- src/CONST.ts | 1 + src/ROUTES.ts | 16 ++++ src/SCREENS.ts | 1 + src/components/TabSelector/TabSelector.tsx | 2 + src/languages/en.ts | 2 + src/languages/es.ts | 2 + .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 18 +++++ src/libs/Navigation/types.ts | 6 ++ src/pages/Debug/DebugDetails.tsx | 4 +- .../Transaction/DebugTransactionPage.tsx | 77 +++++++++++++++++++ .../DebugTransactionViolations.tsx | 71 +++++++++++++++++ 12 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 src/pages/Debug/Transaction/DebugTransactionPage.tsx create mode 100644 src/pages/Debug/Transaction/DebugTransactionViolations.tsx diff --git a/src/CONST.ts b/src/CONST.ts index b6b297f7fb47..ff0ee69d31bc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5858,6 +5858,7 @@ const CONST = { JSON: 'json', REPORT_ACTIONS: 'actions', REPORT_ACTION_PREVIEW: 'preview', + TRANSACTION_VIOLATIONS: 'violations', }, REPORT_IN_LHN_REASONS: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 07a772aa9390..30c73dc9eaa6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1622,6 +1622,22 @@ const ROUTES = { route: 'debug/details/datetime/:fieldName', getRoute: (fieldName: string, fieldValue?: string, backTo?: string) => getUrlWithBackToParam(`debug/details/datetime/${fieldName}?fieldValue=${fieldValue}`, backTo), }, + DEBUG_TRANSACTION: { + route: 'debug/transaction/:transactionID', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}` as const, + }, + DEBUG_TRANSACTION_TAB_DETAILS: { + route: 'debug/transaction/:transactionID/details', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/details` as const, + }, + DEBUG_TRANSACTION_TAB_JSON: { + route: 'debug/transaction/:transactionID/json', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/json` as const, + }, + DEBUG_TRANSACTION_TAB_VIOLATIONS: { + route: 'debug/transaction/:transactionID/violations', + getRoute: (transactionID: string) => `debug/transaction/${transactionID}/violations` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 09cfbca3de9b..34fabe39a4cb 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -584,6 +584,7 @@ const SCREENS = { REPORT_ACTION_CREATE: 'Debug_Report_Action_Create', DETAILS_CONSTANT_PICKER_PAGE: 'Debug_Details_Constant_Picker_Page', DETAILS_DATE_TIME_PICKER_PAGE: 'Debug_Details_Date_Time_Picker_Page', + TRANSACTION: 'Debug_Transaction', }, } as const; diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx index 1bf753cd4aa4..84e2f2f2ee6a 100644 --- a/src/components/TabSelector/TabSelector.tsx +++ b/src/components/TabSelector/TabSelector.tsx @@ -35,6 +35,8 @@ function getIconAndTitle(route: string, translate: LocaleContextProps['translate return {icon: Expensicons.Document, title: translate('debug.reportActions')}; case CONST.DEBUG.REPORT_ACTION_PREVIEW: return {icon: Expensicons.Document, title: translate('debug.reportActionPreview')}; + case CONST.DEBUG.TRANSACTION_VIOLATIONS: + return {icon: Expensicons.Exclamation, title: translate('debug.violations')}; case CONST.TAB_REQUEST.MANUAL: return {icon: Expensicons.Pencil, title: translate('tabSelector.manual')}; case CONST.TAB_REQUEST.SCAN: diff --git a/src/languages/en.ts b/src/languages/en.ts index 343990813197..b1eee495c6de 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5026,6 +5026,8 @@ const translations = { createReportAction: 'Create Report Action', reportAction: 'Report Action', report: 'Report', + transaction: 'Transaction', + violations: 'Violations', hint: "Data changes won't be sent to the backend", textFields: 'Text fields', numberFields: 'Number fields', diff --git a/src/languages/es.ts b/src/languages/es.ts index 50ee28d488c6..099a008c431e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5541,6 +5541,8 @@ const translations = { createReportAction: 'Crear Report Action', reportAction: 'Report Action', report: 'Report', + transaction: 'Transacción', + violations: 'Violaciones', hint: 'Los cambios de datos no se enviarán al backend', textFields: 'Campos de texto', numberFields: 'Campos numéricos', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 3d1b4e3d95b6..7fc39cb97bfb 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -617,6 +617,7 @@ const DebugModalStackNavigator = createModalStackNavigator({ [SCREENS.DEBUG.REPORT_ACTION_CREATE]: () => require('../../../../pages/Debug/ReportAction/DebugReportActionCreatePage').default, [SCREENS.DEBUG.DETAILS_CONSTANT_PICKER_PAGE]: () => require('../../../../pages/Debug/DebugDetailsConstantPickerPage').default, [SCREENS.DEBUG.DETAILS_DATE_TIME_PICKER_PAGE]: () => require('../../../../pages/Debug/DebugDetailsDateTimePickerPage').default, + [SCREENS.DEBUG.TRANSACTION]: () => require('../../../../pages/Debug/Transaction/DebugTransactionPage').default, }); export { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 45d93cd8f57d..1cf2adb17e96 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1298,6 +1298,24 @@ const config: LinkingOptions['config'] = { path: ROUTES.DETAILS_DATE_TIME_PICKER_PAGE.route, exact: true, }, + [SCREENS.DEBUG.TRANSACTION]: { + path: ROUTES.DEBUG_TRANSACTION.route, + exact: true, + screens: { + details: { + path: ROUTES.DEBUG_TRANSACTION_TAB_DETAILS.route, + exact: true, + }, + json: { + path: ROUTES.DEBUG_TRANSACTION_TAB_JSON.route, + exact: true, + }, + violations: { + path: ROUTES.DEBUG_TRANSACTION_TAB_VIOLATIONS.route, + exact: true, + }, + }, + }, }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 372a8cfc27bb..7c87aa811b38 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1558,6 +1558,12 @@ type DebugParamList = { fieldValue?: string; backTo?: string; }; + [SCREENS.DEBUG.TRANSACTION]: { + transactionID: string; + }; + [SCREENS.DEBUG.TRANSACTION]: { + transactionID: string; + }; }; type RootStackParamList = PublicScreensParamList & AuthScreensParamList & LeftModalNavigatorParamList; diff --git a/src/pages/Debug/DebugDetails.tsx b/src/pages/Debug/DebugDetails.tsx index c64e8e3a9331..5515b58655b8 100644 --- a/src/pages/Debug/DebugDetails.tsx +++ b/src/pages/Debug/DebugDetails.tsx @@ -18,7 +18,7 @@ import Navigation from '@libs/Navigation/Navigation'; import Debug from '@userActions/Debug'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report, ReportAction} from '@src/types/onyx'; +import type {Report, ReportAction, Transaction} from '@src/types/onyx'; import type {DetailsConstantFieldsKeys, DetailsDatetimeFieldsKeys, DetailsDisabledKeys} from './const'; import {DETAILS_CONSTANT_FIELDS, DETAILS_DATETIME_FIELDS, DETAILS_DISABLED_KEYS} from './const'; import ConstantSelector from './ConstantSelector'; @@ -26,7 +26,7 @@ import DateTimeSelector from './DateTimeSelector'; type DebugDetailsProps = { /** The report or report action data to be displayed and editted. */ - data: OnyxEntry | OnyxEntry; + data: OnyxEntry | OnyxEntry | OnyxEntry; children?: React.ReactNode; diff --git a/src/pages/Debug/Transaction/DebugTransactionPage.tsx b/src/pages/Debug/Transaction/DebugTransactionPage.tsx new file mode 100644 index 000000000000..3b97d87245a8 --- /dev/null +++ b/src/pages/Debug/Transaction/DebugTransactionPage.tsx @@ -0,0 +1,77 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TabSelector from '@components/TabSelector/TabSelector'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Debug from '@libs/actions/Debug'; +import DebugUtils from '@libs/DebugUtils'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; +import OnyxTabNavigator, {TopTab} from '@libs/Navigation/OnyxTabNavigator'; +import type {DebugParamList} from '@libs/Navigation/types'; +import DebugDetails from '@pages/Debug/DebugDetails'; +import DebugJSON from '@pages/Debug/DebugJSON'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import DebugTransactionViolations from './DebugTransactionViolations'; + +type DebugTransactionPageProps = StackScreenProps; + +function DebugTransactionPage({ + route: { + params: {transactionID}, + }, +}: DebugTransactionPageProps) { + const {translate} = useLocalize(); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + const styles = useThemeStyles(); + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + + + + {() => ( + { + Debug.mergeDebugData(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, data); + }} + onDelete={() => { + Debug.mergeDebugData(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, null); + }} + // TODO: Create DebugUtils.validateTransactionDraftProperty + validate={DebugUtils.validateReportDraftProperty} + /> + )} + + {() => } + {() => } + + + )} + + ); +} + +DebugTransactionPage.displayName = 'DebugTransactionPage'; + +export default DebugTransactionPage; diff --git a/src/pages/Debug/Transaction/DebugTransactionViolations.tsx b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx new file mode 100644 index 000000000000..10e7b9076d31 --- /dev/null +++ b/src/pages/Debug/Transaction/DebugTransactionViolations.tsx @@ -0,0 +1,71 @@ +import React, {useState} from 'react'; +import type {ListRenderItemInfo} from 'react-native'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import FlatList from '@components/FlatList'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DebugUtils from '@libs/DebugUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {TransactionViolation} from '@src/types/onyx'; + +type DebugTransactionViolationsProps = { + transactionID: string; +}; + +function DebugTransactionViolations({transactionID}: DebugTransactionViolationsProps) { + const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [transactionViolationsJSON, setTransactionViolationsJSON] = useState(DebugUtils.onyxDataToString(transactionViolations)); + const numberOfLines = DebugUtils.getNumberOfLinesFromString(transactionViolationsJSON); + + return ( + + {/*