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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] =?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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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/296] 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 0c14e8759de853b483bf22ce1bfc1bd379b2fc3a Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 16 Oct 2024 11:41:51 +0300 Subject: [PATCH 049/296] Reapply changes after merging main --- src/components/Composer/implementation/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/Composer/implementation/index.tsx b/src/components/Composer/implementation/index.tsx index 4431007793cb..741467867c15 100755 --- a/src/components/Composer/implementation/index.tsx +++ b/src/components/Composer/implementation/index.tsx @@ -20,6 +20,7 @@ import updateIsFullComposerAvailable from '@libs/ComposerUtils/updateIsFullCompo import * as EmojiUtils from '@libs/EmojiUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import isEnterWhileComposition from '@libs/KeyboardShortcut/isEnterWhileComposition'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; const excludeNoStyles: Array = []; @@ -72,6 +73,7 @@ function Composer( start: selectionProp.start, end: selectionProp.end, }); + const [hasMultipleLines, setHasMultipleLines] = useState(false); const [isRendered, setIsRendered] = useState(false); const isScrollBarVisible = useIsScrollBarVisible(textInput, value ?? ''); const [prevScroll, setPrevScroll] = useState(); @@ -330,10 +332,10 @@ function Composer( scrollStyleMemo, StyleUtils.getComposerMaxHeightStyle(maxLines, isComposerFullSize), isComposerFullSize ? {height: '100%', maxHeight: 'none'} : undefined, - textContainsOnlyEmojis ? styles.onlyEmojisTextLineHeight : {}, + textContainsOnlyEmojis && hasMultipleLines ? styles.onlyEmojisTextLineHeight : {}, ], - [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], + [style, styles.rtlTextRenderForSafari, styles.onlyEmojisTextLineHeight, scrollStyleMemo, hasMultipleLines, StyleUtils, maxLines, isComposerFullSize, textContainsOnlyEmojis], ); return ( @@ -353,6 +355,7 @@ function Composer( {...props} onSelectionChange={addCursorPositionToSelectionChange} onContentSizeChange={(e) => { + setHasMultipleLines(e.nativeEvent.contentSize.height > variables.componentSizeLarge); updateIsFullComposerAvailable({maxLines, isComposerFullSize, isDisabled, setIsFullComposerAvailable}, e, styles); }} disabled={isDisabled} From 51842c47fb01dc9373f19ef7537aec5c20d88a35 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sun, 20 Oct 2024 18:03:09 +0530 Subject: [PATCH 050/296] minor updates. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 6c87d9f3d559..5097d34111c7 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); const isApproved = ReportUtils.isReportApproved(iouReport, action); - const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0); const thumbsUpStyle = useAnimatedStyle(() => ({ ...styles.defaultCheckmarkWrapper, transform: [{scale: thumbsUpScale.value}], @@ -483,7 +483,7 @@ function ReportPreview({ - {previewMessage} + {previewMessage} {shouldShowRBR && ( Date: Sun, 20 Oct 2024 18:07:14 +0530 Subject: [PATCH 051/296] make animation subtle. Signed-off-by: krishna2323 --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 5097d34111c7..9ebcf792ed5e 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -139,7 +139,7 @@ function ReportPreview({ const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); const isApproved = ReportUtils.isReportApproved(iouReport, action); - const thumbsUpScale = useSharedValue(isApproved ? 1 : 0); + const thumbsUpScale = useSharedValue(isApproved ? 1 : 0.25); const thumbsUpStyle = useAnimatedStyle(() => ({ ...styles.defaultCheckmarkWrapper, transform: [{scale: thumbsUpScale.value}], From eee882cb68a3299a1d348997db5bbebea1cf1bcd Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 11:08:17 +0700 Subject: [PATCH 052/296] fix: show video control when video ended --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 012537b75108..1970a2692e90 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -70,6 +70,7 @@ function BaseVideoPlayer({ const [position, setPosition] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [isEnded, setIsEnded] = useState(false); const [isBuffering, setIsBuffering] = useState(true); // we add "#t=0.001" at the end of the URL to skip first milisecond of the video and always be able to show proper video preview when video is paused at the beginning const [sourceURL] = useState(VideoUtils.addSkipTimeTagToURL(url.includes('blob:') || url.includes('file:///') ? url : addEncryptedAuthTokenToURL(url), 0.001)); @@ -199,6 +200,8 @@ function BaseVideoPlayer({ return; } + setIsEnded(status.didJustFinish && !status.isLooping); + if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { updateVolume(0.25); } @@ -456,7 +459,7 @@ function BaseVideoPlayer({ {((isLoading && !isOffline) || (isBuffering && !isPlaying)) && } {isLoading && (isOffline || !isBuffering) && } - {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && ( + {controlStatusState !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen || isEnded) && ( Date: Tue, 22 Oct 2024 11:43:00 +0700 Subject: [PATCH 053/296] fix: sorted suggestion emoji --- src/libs/EmojiUtils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 7c042bbefe67..8f901ac0ed74 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -1,4 +1,5 @@ import {Str} from 'expensify-common'; +import lodashSortBy from 'lodash/sortBy'; import Onyx from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import * as Emojis from '@assets/emojis'; @@ -424,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } @@ -434,7 +435,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } for (const suggestion of suggestions) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } if (!matching.find((obj) => obj.name === suggestion.name)) { @@ -442,7 +443,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } } } - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } /** From 596f16f64f64b75595f29efb76e0cea93c7d891e Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:34:55 +0700 Subject: [PATCH 054/296] fix test --- src/libs/EmojiUtils.ts | 2 +- src/libs/Firebase/index.web.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 8f901ac0ed74..bf5d611b1a73 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -425,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return matching; } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index 2d42154d3c26..d643dc48ab27 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - Object.entries(attributes).forEach(([name, value]) => { - perfTrace.putAttribute(name, value); - }); + // Object.entries(attributes).forEach(([name, value]) => { + // perfTrace.putAttribute(name, value); + // }); traceMap[customEventName] = { trace: perfTrace, From 313d716179a0f588b114cefab506ef5043a64f0d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:35:19 +0700 Subject: [PATCH 055/296] chore --- src/libs/Firebase/index.web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index d643dc48ab27..2d42154d3c26 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - // Object.entries(attributes).forEach(([name, value]) => { - // perfTrace.putAttribute(name, value); - // }); + Object.entries(attributes).forEach(([name, value]) => { + perfTrace.putAttribute(name, value); + }); traceMap[customEventName] = { trace: perfTrace, From 8e6b36067b820412f56f7d6a95c2315affbd08d3 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:47:59 +0700 Subject: [PATCH 056/296] fix test --- src/libs/EmojiUtils.ts | 2 +- src/libs/Firebase/index.web.ts | 6 +++--- tests/unit/EmojiTest.ts | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index bf5d611b1a73..8f901ac0ed74 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -425,7 +425,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return matching; + return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index 2d42154d3c26..d643dc48ab27 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - Object.entries(attributes).forEach(([name, value]) => { - perfTrace.putAttribute(name, value); - }); + // Object.entries(attributes).forEach(([name, value]) => { + // perfTrace.putAttribute(name, value); + // }); traceMap[customEventName] = { trace: perfTrace, diff --git a/tests/unit/EmojiTest.ts b/tests/unit/EmojiTest.ts index c96228b49fbc..2033085c5694 100644 --- a/tests/unit/EmojiTest.ts +++ b/tests/unit/EmojiTest.ts @@ -154,6 +154,11 @@ describe('EmojiTest', () => { it('correct suggests emojis accounting for keywords', () => { const thumbEmojisEn: Emoji[] = [ + { + name: 'hand_with_index_finger_and_thumb_crossed', + code: '🫰', + types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'], + }, { code: '👍', name: '+1', @@ -164,11 +169,6 @@ describe('EmojiTest', () => { name: '-1', types: ['👎🏿', '👎🏾', '👎🏽', '👎🏼', '👎🏻'], }, - { - name: 'hand_with_index_finger_and_thumb_crossed', - code: '🫰', - types: ['🫰🏿', '🫰🏾', '🫰🏽', '🫰🏼', '🫰🏻'], - }, ]; const thumbEmojisEs: Emoji[] = [ From 7cacc00521af3c4b7bd97df155db2fab95e075da Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 22 Oct 2024 15:49:51 +0700 Subject: [PATCH 057/296] fix lint --- src/libs/Firebase/index.web.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/Firebase/index.web.ts b/src/libs/Firebase/index.web.ts index d643dc48ab27..2d42154d3c26 100644 --- a/src/libs/Firebase/index.web.ts +++ b/src/libs/Firebase/index.web.ts @@ -21,9 +21,9 @@ const startTrace: StartTrace = (customEventName) => { const attributes = utils.getAttributes(); - // Object.entries(attributes).forEach(([name, value]) => { - // perfTrace.putAttribute(name, value); - // }); + Object.entries(attributes).forEach(([name, value]) => { + perfTrace.putAttribute(name, value); + }); traceMap[customEventName] = { trace: perfTrace, From 33d1ba31a3d0f1ec31e749eba325ed03c66918b2 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sat, 26 Oct 2024 13:59:08 +0530 Subject: [PATCH 058/296] Update --- tests/ui/GroupChatNameTests.tsx | 4 ---- tests/utils/TestHelper.ts | 29 +++++------------------------ 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 25defa2ff283..393cf2c6dad2 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -41,10 +41,6 @@ let transitionEndCB: () => void; jest.mock('@react-navigation/native'); -beforeAll(() => { - TestHelper.beforeAllSetupUITests(); -}); - const REPORT_ID = '1'; const USER_A_ACCOUNT_ID = 1; const USER_A_EMAIL = 'user_a@test.com'; diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts index 084edc1d1567..394c05fe8b7c 100644 --- a/tests/utils/TestHelper.ts +++ b/tests/utils/TestHelper.ts @@ -313,30 +313,12 @@ function assertFormDataMatchesObject(obj: Report, formData?: FormData) { 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?`, - }); + const optionRow = screen.queryAllByAccessibilityHint(hintText).at(index); + if (!optionRow) { + return; } + fireEvent(optionRow, 'press'); + await waitForBatchedUpdatesWithAct(); } export type {MockFetch, FormData}; @@ -353,5 +335,4 @@ export { expectAPICommandToHaveBeenCalledWith, setupGlobalFetchMock, navigateToSidebarOption, - beforeAllSetupUITests, }; From 304f0d2fc357e24e88006c0a35f821dc1f1639c1 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Oct 2024 19:05:35 +0530 Subject: [PATCH 059/296] Update --- tests/ui/GroupChatNameTests.tsx | 3 +++ tests/utils/LHNTestUtils.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 393cf2c6dad2..3c13605925c9 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -41,6 +41,8 @@ let transitionEndCB: () => void; jest.mock('@react-navigation/native'); +TestHelper.setupApp(); + const REPORT_ID = '1'; const USER_A_ACCOUNT_ID = 1; const USER_A_EMAIL = 'user_a@test.com'; @@ -69,6 +71,7 @@ function signInAndGetApp(reportName = '', participantAccountIDs?: number[]): Pro const participants: Record = {}; participantAccountIDs?.forEach((id) => { participants[id] = { + notificationPreference: 'always', hidden: false, role: id === 1 ? CONST.REPORT.ROLE.ADMIN : CONST.REPORT.ROLE.MEMBER, } as Participant; diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 7a9873483069..02223d36f3c6 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -134,7 +134,7 @@ function getFakeReport(participantAccountIDs = [1, 2], millisecondsInThePast = 0 adminIDs.forEach((id) => { participants[id] = { - hidden: false, + notificationPreference: 'always', role: CONST.REPORT.ROLE.ADMIN, }; }); From e24e1ccd2c10cc2fff1f3addc8240f429d4f8d51 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Tue, 29 Oct 2024 21:50:05 +0530 Subject: [PATCH 060/296] Update --- __mocks__/@react-native-reanimated/index.ts | 11 ----------- tests/ui/GroupChatNameTests.tsx | 4 ++-- 2 files changed, 2 insertions(+), 13 deletions(-) delete mode 100644 __mocks__/@react-native-reanimated/index.ts diff --git a/__mocks__/@react-native-reanimated/index.ts b/__mocks__/@react-native-reanimated/index.ts deleted file mode 100644 index df9cc4ecef8d..000000000000 --- a/__mocks__/@react-native-reanimated/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ - -const actualAnimated = jest.requireActual('react-native-reanimated/mock'); - -const mock = { - ...actualAnimated, - createAnimatedPropAdapter: jest.fn(), - useReducedMotion: jest.fn(), -}; - -export default mock; diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 3c13605925c9..919279ad00e3 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -174,7 +174,7 @@ describe('Tests for group chat name', () => { 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', () => + it('Should show limited names in LHN when 8 participants are present', () => signInAndGetApp('', participantAccountIDs8).then(() => { // Verify the sidebar links are rendered const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); @@ -189,7 +189,7 @@ describe('Tests for group chat name', () => { 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')); + return waitFor(() => expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E')); })); it('Check if group name shows fine for report header', () => From cf582b458f571ed64722c10056b3b86fd797ed0f Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:15:52 +0700 Subject: [PATCH 061/296] fix: Update second Allow location access modal on web --- src/components/LocationPermissionModal/index.tsx | 15 ++++++++++----- src/components/LocationPermissionModal/types.ts | 4 +--- src/languages/en.ts | 1 + src/languages/es.ts | 1 + 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx index 0e500a9b7cc4..fcb7cdacbd4c 100644 --- a/src/components/LocationPermissionModal/index.tsx +++ b/src/components/LocationPermissionModal/index.tsx @@ -39,10 +39,10 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe if (hasError) { if (Linking.openSettings) { Linking.openSettings(); + } else { + onDeny?.(); } setShowModal(false); - setHasError(false); - resetPermissionFlow(); return; } cb(); @@ -54,7 +54,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe if (status === RESULTS.GRANTED || status === RESULTS.LIMITED) { onGrant(); } else { - onDeny(status); + onDeny(); } }) .finally(() => { @@ -64,7 +64,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }); const skipLocationPermission = () => { - onDeny(RESULTS.DENIED); + onDeny(); setShowModal(false); setHasError(false); }; @@ -83,13 +83,17 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }; return ( { + setHasError(false); + resetPermissionFlow(); + }} isVisible={showModal} onConfirm={grantLocationPermission} onCancel={skipLocationPermission} onBackdropPress={closeModal} confirmText={getConfirmText()} cancelText={translate('common.notNow')} - prompt={translate(hasError ? 'receipt.locationErrorMessage' : 'receipt.locationAccessMessage')} promptStyles={[styles.textLabelSupportingEmptyValue, styles.mb4]} title={translate(hasError ? 'receipt.locationErrorTitle' : 'receipt.locationAccessTitle')} titleContainerStyles={[styles.mt2, styles.mb0]} @@ -100,6 +104,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe iconHeight={120} shouldCenterIcon shouldReverseStackedButtons + prompt={translate(hasError ? (isWeb ? 'receipt.allowLocationFromSetting' : 'receipt.locationErrorMessage') : 'receipt.locationAccessMessage')} /> ); } diff --git a/src/components/LocationPermissionModal/types.ts b/src/components/LocationPermissionModal/types.ts index ec603bfdb8c1..eb18e1d71c13 100644 --- a/src/components/LocationPermissionModal/types.ts +++ b/src/components/LocationPermissionModal/types.ts @@ -1,11 +1,9 @@ -import type {PermissionStatus} from 'react-native-permissions'; - type LocationPermissionModalProps = { /** A callback to call when the permission has been granted */ onGrant: () => void; /** A callback to call when the permission has been denied */ - onDeny: (permission: PermissionStatus) => void; + onDeny: () => void; /** Should start the permission flow? */ startPermissionFlow: boolean; diff --git a/src/languages/en.ts b/src/languages/en.ts index 4c68da68ede6..3b2563e73dda 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -786,6 +786,7 @@ const translations = { locationAccessMessage: 'Location access helps us keep your timezone and currency accurate wherever you go.', locationErrorTitle: 'Allow location access', locationErrorMessage: 'Location access helps us keep your timezone and currency accurate wherever you go.', + allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, dropTitle: 'Let it go', dropMessage: 'Drop your file here', flash: 'flash', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4aed242db5fa..d9b0e96e0390 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -779,6 +779,7 @@ const translations = { locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', locationErrorTitle: 'Permitir acceso a la ubicación', locationErrorMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', + allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, cameraErrorMessage: 'Se ha producido un error al hacer una foto. Por favor, inténtalo de nuevo.', dropTitle: 'Suéltalo', dropMessage: 'Suelta tu archivo aquí', From 379f9c56197048cf04a6d6d74d75f2561c24a6ad Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:23:17 +0700 Subject: [PATCH 062/296] fix: type --- src/components/LocationPermissionModal/index.android.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/LocationPermissionModal/index.android.tsx b/src/components/LocationPermissionModal/index.android.tsx index 30896cf37084..6e4e6877c540 100644 --- a/src/components/LocationPermissionModal/index.android.tsx +++ b/src/components/LocationPermissionModal/index.android.tsx @@ -50,7 +50,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe setHasError(true); return; } else { - onDeny(status); + onDeny(); } setShowModal(false); setHasError(false); @@ -58,7 +58,7 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe }); const skipLocationPermission = () => { - onDeny(RESULTS.DENIED); + onDeny(); setShowModal(false); setHasError(false); }; From a34d504ff4c0e33ea26f5a2a109e34539a124c28 Mon Sep 17 00:00:00 2001 From: truph01 Date: Wed, 30 Oct 2024 08:37:00 +0700 Subject: [PATCH 063/296] fix: lint --- src/components/LocationPermissionModal/index.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/LocationPermissionModal/index.tsx b/src/components/LocationPermissionModal/index.tsx index fcb7cdacbd4c..45e3f5b22d1b 100644 --- a/src/components/LocationPermissionModal/index.tsx +++ b/src/components/LocationPermissionModal/index.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Linking} from 'react-native'; import {RESULTS} from 'react-native-permissions'; import ConfirmModal from '@components/ConfirmModal'; @@ -81,6 +81,9 @@ function LocationPermissionModal({startPermissionFlow, resetPermissionFlow, onDe setShowModal(false); resetPermissionFlow(); }; + + const locationErrorMessage = useMemo(() => (isWeb ? 'receipt.allowLocationFromSetting' : 'receipt.locationErrorMessage'), [isWeb]); + return ( ); } From c0a7a2f31e8b5c5a943720f9cfb045c18a876121 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 30 Oct 2024 16:50:58 +0700 Subject: [PATCH 064/296] create a function to remove dup code --- src/libs/EmojiUtils.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 8f901ac0ed74..f9fb5f226280 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -24,6 +24,8 @@ const findEmojiByName = (name: string): Emoji => Emojis.emojiNameTable[name]; const findEmojiByCode = (code: string): Emoji => Emojis.emojiCodeTableWithSkinTones[code]; +const sortByName = (emoji: Emoji, emojiData: RegExpMatchArray) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1)); + let frequentlyUsedEmojis: FrequentlyUsedEmoji[] = []; Onyx.connect({ key: ONYXKEYS.FREQUENTLY_USED_EMOJIS, @@ -425,7 +427,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO for (const node of nodes) { if (node.metaData?.code && !matching.find((obj) => obj.name === node.name)) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } matching.push({code: node.metaData.code, name: node.name, types: node.metaData.types}); } @@ -435,7 +437,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } for (const suggestion of suggestions) { if (matching.length === limit) { - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } if (!matching.find((obj) => obj.name === suggestion.name)) { @@ -443,7 +445,7 @@ function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_CO } } } - return lodashSortBy(matching, (emoji) => !emoji.name.includes(emojiData[0].toLowerCase().slice(1))); + return lodashSortBy(matching, (emoji) => sortByName(emoji, emojiData)); } /** From a8b9955c47368b50588b7bbbb27026d71b855e32 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:57:33 +0530 Subject: [PATCH 065/296] Update --- tests/ui/GroupChatNameTests.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/GroupChatNameTests.tsx b/tests/ui/GroupChatNameTests.tsx index 919279ad00e3..fc383efe4e28 100644 --- a/tests/ui/GroupChatNameTests.tsx +++ b/tests/ui/GroupChatNameTests.tsx @@ -236,7 +236,7 @@ describe('Tests for group chat name', () => { 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'); + expect(displayNameText?.props?.children?.[0]).toBe('A, B, C, D, E'); return navigateToSidebarOption(0); }) From 0705ceed3a258cda1fb33e819b662b56539fd78a Mon Sep 17 00:00:00 2001 From: Ugo Giordano <144606237+ugogiordano@users.noreply.github.com> Date: Wed, 30 Oct 2024 14:02:41 +0100 Subject: [PATCH 066/296] Fixing duplicate Concierge chat Addressing the issue of duplicate Concierge chats appearing in the Left Hand Navigation (LHN) when deep linking to Concierge. --- src/pages/ConciergePage.tsx | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/pages/ConciergePage.tsx b/src/pages/ConciergePage.tsx index 46f17e76c083..7ab8deef1bef 100644 --- a/src/pages/ConciergePage.tsx +++ b/src/pages/ConciergePage.tsx @@ -1,9 +1,8 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, useOnyx,withOnyx} from 'react-native-onyx'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -33,21 +32,23 @@ function ConciergePage({session}: ConciergePageProps) { const styles = useThemeStyles(); const isUnmounted = useRef(false); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA, {initialValue: true}); - useFocusEffect(() => { - if (session && 'authToken' in session) { - App.confirmReadyToOpenApp(); - // Pop the concierge loading page before opening the concierge report. - Navigation.isNavigationReady().then(() => { - if (isUnmounted.current) { - return; - } - Report.navigateToConciergeChat(true, () => !isUnmounted.current); - }); - } else { - Navigation.navigate(); - } - }); + useFocusEffect( + useCallback(() => { + if (session && 'authToken' in session) { + App.confirmReadyToOpenApp(); + Navigation.isNavigationReady().then(() => { + if (isUnmounted.current || isLoadingReportData === undefined || !!isLoadingReportData) { + return; + } + Report.navigateToConciergeChat(true, () => !isUnmounted.current); + }); + } else { + Navigation.navigate(); + } + }, [session, isLoadingReportData]), + ); useEffect(() => { isUnmounted.current = false; From 4d20eaa6e1b42b0d4e94f21e603cfe0cd42a3fc0 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 30 Oct 2024 23:35:13 +0700 Subject: [PATCH 067/296] fix show video controler in native --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 1970a2692e90..fad641e696ae 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -96,6 +96,7 @@ function BaseVideoPlayer({ const shouldUseNewRate = typeof source === 'number' || !source || source.uri !== sourceURL; const togglePlayCurrentVideo = useCallback(() => { + setIsEnded(false); videoResumeTryNumberRef.current = 0; if (!isCurrentlyURLSet) { updateCurrentlyPlayingURL(url); @@ -107,9 +108,12 @@ function BaseVideoPlayer({ }, [isCurrentlyURLSet, isPlaying, pauseVideo, playVideo, updateCurrentlyPlayingURL, url, videoResumeTryNumberRef]); const hideControl = useCallback(() => { + if (isEnded) { + return; + } // eslint-disable-next-line react-compiler/react-compiler controlsOpacity.value = withTiming(0, {duration: 500}, () => runOnJS(setControlStatusState)(CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE)); - }, [controlsOpacity]); + }, [controlsOpacity, isEnded]); const debouncedHideControl = useMemo(() => debounce(hideControl, 1500), [hideControl]); useEffect(() => { @@ -199,8 +203,11 @@ function BaseVideoPlayer({ onPlaybackStatusUpdate?.(status); return; } - - setIsEnded(status.didJustFinish && !status.isLooping); + if (status.didJustFinish) { + setIsEnded(status.didJustFinish && !status.isLooping); + setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); + controlsOpacity.value = 1; + } if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { updateVolume(0.25); From b64f63db80eb53a9e6dab3c735f7293557daa53b Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 31 Oct 2024 01:41:41 +0530 Subject: [PATCH 068/296] Update timeout of UnreadIndicatorsTest.tsx --- tests/ui/UnreadIndicatorsTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index bada60720e36..4cb2b5463fb9 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -30,7 +30,7 @@ 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(30000); +jest.setTimeout(40000); jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); From bd9f6e8077ce434b1b034c45e195928dc94c81ab Mon Sep 17 00:00:00 2001 From: Shridhar Goel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 31 Oct 2024 01:42:35 +0530 Subject: [PATCH 069/296] Update timeout of UnreadIndicatorsTest.tsx --- tests/ui/UnreadIndicatorsTest.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.tsx b/tests/ui/UnreadIndicatorsTest.tsx index 4cb2b5463fb9..c201d8cabd74 100644 --- a/tests/ui/UnreadIndicatorsTest.tsx +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -30,7 +30,7 @@ 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(40000); +jest.setTimeout(45000); jest.mock('@react-navigation/native'); jest.mock('../../src/libs/Notification/LocalNotification'); From e602e696743a5f2554fc5cf4f86872776ac47e93 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 04:56:08 +0530 Subject: [PATCH 070/296] pass 'canUserPerformWriteAction' to shouldReportActionBeVisible. Signed-off-by: krishna2323 --- .../AttachmentCarousel/extractAttachments.ts | 5 +- .../LHNOptionsList/LHNOptionsList.tsx | 4 +- src/components/ParentNavigationSubtitle.tsx | 5 +- src/hooks/usePaginatedReportActions.ts | 5 +- src/libs/OptionsListUtils.ts | 7 +- src/libs/ReportActionsUtils.ts | 33 ++--- src/libs/actions/Report.ts | 21 +++- src/pages/Debug/Report/DebugReportActions.tsx | 5 +- src/pages/home/ReportScreen.tsx | 6 +- .../report/ReportActionItemParentAction.tsx | 113 +++++++++--------- src/pages/home/report/ReportActionsList.tsx | 4 +- src/pages/home/report/ReportActionsView.tsx | 3 +- src/pages/home/report/ThreadDivider.tsx | 8 +- .../perf-test/ReportActionsUtils.perf-test.ts | 8 +- tests/unit/ReportActionsUtilsTest.ts | 6 +- 15 files changed, 137 insertions(+), 96 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 69b0b8229f4a..5ebaf8af673e 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -4,6 +4,7 @@ import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import * as FileUtils from '@libs/fileDownload/FileUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; import type {ReportAction, ReportActions} from '@src/types/onyx'; @@ -24,6 +25,8 @@ function extractAttachments( ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; const attachments: Attachment[] = []; + const report = ReportUtils.getReport(reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // We handle duplicate image sources by considering the first instance as original. Selecting any duplicate // and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position. @@ -96,7 +99,7 @@ function extractAttachments( const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; actions.forEach((action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID) || ReportActionsUtils.isMoneyRequestAction(action)) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action),) { return; } diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index b317d3020e2a..c03d7822f9e3 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -19,6 +19,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import * as DraftCommentUtils from '@libs/DraftCommentUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -139,7 +140,8 @@ 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, reportID); + + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, reportID, ReportUtils.canUserPerformWriteAction(itemFullReport)); const lastReportAction = sortedReportActions.at(0); // Get the transaction for the last report action diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index ef0f981a8c77..248eaec9c9b1 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -5,6 +5,7 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; import ROUTES from '@src/ROUTES'; @@ -29,6 +30,8 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct const {workspaceName, reportName} = parentNavigationSubtitleData; const {isOffline} = useNetwork(); const {translate} = useLocalize(); + const report = ReportUtils.getReport(parentReportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // We should not display the parent navigation subtitle if the user does not have access to the parent chat (the reportName is empty in this case) if (!reportName) { @@ -39,7 +42,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct { const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1'); - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', parentReportID); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', parentReportID, canUserPerformWriteAction); // 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/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index 342d73b08bd8..6b476db46d7d 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -2,6 +2,7 @@ import {useMemo} from 'react'; import {useOnyx} from 'react-native-onyx'; import PaginationUtils from '@libs/PaginationUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; /** @@ -11,10 +12,12 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) { // Use `||` instead of `??` to handle empty string. // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportIDWithDefault = reportID || '-1'; + const report = ReportUtils.getReport(reportIDWithDefault); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID ?? '-1', true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID ?? '-1', canUserPerformWriteAction, true), }); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index df4b75893336..9fd121fdfb5b 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -328,11 +328,14 @@ Onyx.connect({ lastReportActions[reportID] = firstReportAction; } + const report = ReportUtils.getReport(reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + // The report is only visible if it is the last action not deleted that // does not match a closed or created state. const reportActionsForDisplay = sortedReportActions.filter( (reportAction, actionKey) => - ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, reportID) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, reportID, canUserPerformWriteAction) && !ReportActionUtils.isWhisperAction(reportAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && @@ -590,7 +593,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, reportID) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, key, reportID, ReportUtils.canUserPerformWriteAction(report)) && 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 be0efc67ce5b..7778cb29d552 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 type {OptimisticIOUReportAction, PartialReportAction} from './ReportUtils'; -import {canUserPerformWriteAction, getReport} from './ReportUtils'; import StringUtils from './StringUtils'; // eslint-disable-next-line import/no-cycle import * as TransactionUtils from './TransactionUtils'; @@ -636,12 +635,8 @@ 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, reportID: string): boolean { - const report = getReport(reportID); - if ( - (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(report?.reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && - !canUserPerformWriteAction(report) - ) { +function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, reportID: string, canUserPerformWriteAction?: boolean): boolean { + if ((isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && !canUserPerformWriteAction) { return false; } @@ -720,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, reportID: string): boolean { +function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry, reportID: string, canUserPerformWriteAction?: boolean): boolean { if (!reportAction) { return false; } @@ -732,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, reportID) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, reportID, canUserPerformWriteAction) && !(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) && !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction)) && !isResolvedActionTrackExpense(reportAction) @@ -765,7 +760,7 @@ function replaceBaseURLInPolicyChangeLogAction(reportAction: ReportAction): Repo return updatedReportAction; } -function getLastVisibleAction(reportID: string, actionsToMerge: Record | null> = {}): OnyxEntry { +function getLastVisibleAction(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}): OnyxEntry { let reportActions: Array = []; if (!isEmpty(actionsToMerge)) { reportActions = Object.values(fastMerge(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}, actionsToMerge ?? {}, true)) as Array< @@ -774,7 +769,7 @@ function getLastVisibleAction(reportID: string, actionsToMerge: Record shouldReportActionBeVisibleAsLastAction(action, reportID)); + const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, reportID, canUserPerformWriteAction)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -796,10 +791,11 @@ function formatLastMessageText(lastMessageText: string) { function getLastVisibleMessage( reportID: string, + canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}, reportAction: OnyxInputOrEntry | undefined = undefined, ): LastVisibleMessage { - const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, actionsToMerge); + const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); const message = getReportActionMessage(lastVisibleAction); if (message && isReportMessageAttachment(message)) { @@ -840,7 +836,12 @@ function filterOutDeprecatedReportActions(reportActions: OnyxEntry | ReportAction[], reportID: string, shouldIncludeInvisibleActions = false): ReportAction[] { +function getSortedReportActionsForDisplay( + reportActions: OnyxEntry | ReportAction[], + reportID: string, + canUserPerformWriteAction?: boolean, + shouldIncludeInvisibleActions = false, +): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { return []; @@ -850,7 +851,7 @@ function getSortedReportActionsForDisplay(reportActions: OnyxEntry shouldReportActionBeVisible(reportAction, key, reportID)) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, reportID, canUserPerformWriteAction)) .map(([, reportAction]) => reportAction); } @@ -1099,9 +1100,9 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn * When we delete certain reports, we want to check whether there are any visible actions left to display. * If there are no visible actions left (including system messages), we can hide the report from view entirely */ -function doesReportHaveVisibleActions(reportID: string, actionsToMerge: ReportActions = {}): boolean { +function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteAction: boolean, 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, reportID)); + const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action, reportID, canUserPerformWriteAction)); // Exclude the task system message and the created message const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action)); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index c219137e210c..03ad1de54b5d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -279,7 +279,11 @@ registerPaginationConfig({ nextCommand: READ_COMMANDS.GET_NEWER_ACTIONS, resourceCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS, pageCollectionKey: ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES, - sortItems: (reportActions, reportID) => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportID, true), + sortItems: (reportActions, reportID) => { + const report = ReportUtils.getReport(reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + return ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportID, canUserPerformWriteAction, true); + }, getItemID: (reportAction) => reportAction.reportActionID, }); @@ -1450,8 +1454,10 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { lastVisibleActionCreated: '', }; const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions as ReportActions); + const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); if (lastMessageText || lastMessageTranslationKey) { - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions as ReportActions); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, canUserPerformWriteAction, optimisticReportActions as ReportActions); const lastVisibleActionCreated = lastVisibleAction?.created; const lastActorAccountID = lastVisibleAction?.actorAccountID; optimisticReport = { @@ -1461,7 +1467,6 @@ function deleteReportComment(reportID: string, reportAction: ReportAction) { lastActorAccountID, }; } - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; const didCommentMentionCurrentUser = ReportActionsUtils.didMessageMentionCurrentUser(reportAction); if (didCommentMentionCurrentUser && reportAction.created === report?.lastMentionedTime) { const reportActionsForReport = allReportActions?.[reportID]; @@ -1592,6 +1597,8 @@ function handleUserDeletedLinksInHtml(newCommentText: string, originalCommentMar /** Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. */ function editReportComment(reportID: string, originalReportAction: OnyxEntry, textForNewComment: string, videoAttributeCache?: Record) { const originalReportID = ReportUtils.getOriginalReportID(reportID, originalReportAction); + const report = ReportUtils.getReport(originalReportID ?? '-1'); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); if (!originalReportID || !originalReportAction) { return; @@ -1657,7 +1664,7 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, canUserPerformWriteAction, true), }); const renderItem = ({item}: ListRenderItemInfo) => ( = CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0); const isLinkedActionDeleted = useMemo( - () => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, report?.reportID ?? '-1'), - [linkedAction, report?.reportID], + () => + !!linkedAction && + !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, report?.reportID ?? '-1', ReportUtils.canUserPerformWriteAction(report)), + [linkedAction, report], ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); const isLinkedActionInaccessibleWhisper = useMemo( diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 98cd0448e047..67e16e5b068c 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -106,61 +106,66 @@ function ReportActionItemParentAction({ {/* eslint-disable-next-line react-compiler/react-compiler */} - {allAncestors.map((ancestor) => ( - Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} - > - - {ReportActionsUtils.isTripPreview(ancestor?.reportAction) ? ( - - - - ) : ( - { - 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) { - // Pop the chat report screen before navigating to the linked report action. - Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1', ancestor.reportAction.reportActionID)); - } - } - : undefined - } - parentReportAction={parentReportAction} - report={ancestor.report} - reportActions={reportActions} - transactionThreadReport={transactionThreadReport} - action={ancestor.reportAction} - displayAsGroup={false} - isMostRecentIOUReportAction={false} - shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} - index={index} - isFirstVisibleReportAction={isFirstVisibleReportAction} - shouldUseThreadDividerLine={shouldUseThreadDividerLine} - hideThreadReplies + {allAncestors.map((ancestor) => { + const ancestorReport = ReportUtils.getReport(ancestor.report.reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(ancestorReport); + return ( + Report.navigateToConciergeChatAndDeleteReport(ancestor.report.reportID)} + > + - )} - - ))} + {ReportActionsUtils.isTripPreview(ancestor?.reportAction) ? ( + + + + ) : ( + { + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( + ancestor.reportAction, + ancestor.reportAction.reportActionID ?? '-1', + ancestor.report.reportID, + canUserPerformWriteAction, + ); + // 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) { + // Pop the chat report screen before navigating to the linked report action. + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID ?? '-1', ancestor.reportAction.reportActionID)); + } + } + : undefined + } + parentReportAction={parentReportAction} + report={ancestor.report} + reportActions={reportActions} + transactionThreadReport={transactionThreadReport} + action={ancestor.reportAction} + displayAsGroup={false} + isMostRecentIOUReportAction={false} + shouldDisplayNewMarker={ancestor.shouldDisplayNewMarker} + index={index} + isFirstVisibleReportAction={isFirstVisibleReportAction} + shouldUseThreadDividerLine={shouldUseThreadDividerLine} + hideThreadReplies + /> + )} + + ); + })} {shouldDisplayReplyDivider && } ); diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index 22eda76df1de..95ddd3f561f5 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -195,9 +195,9 @@ function ReportActionsList({ ReportActionsUtils.isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, report.reportID), + ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, report.reportID, ReportUtils.canUserPerformWriteAction(report)), ), - [sortedReportActions, isOffline, report.reportID], + [sortedReportActions, isOffline, report], ); const lastAction = sortedVisibleReportActions.at(0); const sortedVisibleReportActionsObjects: OnyxTypes.ReportActions = useMemo( diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index b58de22bc520..fb1f848b677c 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -90,7 +90,8 @@ 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, report.reportID, true), + selector: (reportActions: OnyxEntry) => + ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, report.reportID, ReportUtils.canUserPerformWriteAction(report), 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 6fec617e4c37..290d173d9a5c 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {Ancestor} from '@libs/ReportUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -47,7 +48,12 @@ function ThreadDivider({ancestor, isLinkDisabled = false}: ThreadDividerProps) { ) : ( { - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1', ancestor.report.reportID); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( + ancestor.reportAction, + ancestor.reportAction.reportActionID ?? '-1', + ancestor.report.reportID, + ReportUtils.canUserPerformWriteAction(ancestor.report), + ); // 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 5e258436edc7..d6c573e846e8 100644 --- a/tests/perf-test/ReportActionsUtils.perf-test.ts +++ b/tests/perf-test/ReportActionsUtils.perf-test.ts @@ -89,11 +89,11 @@ describe('ReportActionsUtils', () => { } as unknown as ReportActions; await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, actionsToMerge)); + await measureFunction(() => ReportActionsUtils.getLastVisibleAction(reportId, true, actionsToMerge)); }); test('[ReportActionsUtils] getMostRecentIOURequestActionID on 10k ReportActions', async () => { - const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId); + const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId, true); await waitForBatchedUpdates(); await measureFunction(() => ReportActionsUtils.getMostRecentIOURequestActionID(reportActionsArray)); @@ -127,12 +127,12 @@ describe('ReportActionsUtils', () => { } as unknown as ReportActions; await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, actionsToMerge)); + await measureFunction(() => ReportActionsUtils.getLastVisibleMessage(reportId, true, actionsToMerge)); }); test('[ReportActionsUtils] getSortedReportActionsForDisplay on 10k ReportActions', async () => { await waitForBatchedUpdates(); - await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId)); + await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportId, true)); }); test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 5f3f14971d8e..ef74f34f6413 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -306,7 +306,7 @@ describe('ReportActionsUtils', () => { // eslint-disable-next-line rulesdir/prefer-at const expectedOutput: ReportAction[] = [...input.slice(0, 1), ...input.slice(2), input[1]]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, "1"); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); expect(result).toStrictEqual(expectedOutput); }); @@ -401,7 +401,7 @@ describe('ReportActionsUtils', () => { // eslint-disable-next-line rulesdir/prefer-at const expectedOutput: ReportAction[] = [...input.slice(0, 1), ...input.slice(2, -1), input[1]]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); expect(result).toStrictEqual(expectedOutput); }); @@ -445,7 +445,7 @@ describe('ReportActionsUtils', () => { message: [{html: '', type: 'Action type', text: 'Action text'}], }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1'); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); input.pop(); expect(result).toStrictEqual(input); }); From 7b32415551e04a96e2b0d7b75378c5350bc4cfbc Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:00:04 +0530 Subject: [PATCH 071/296] fix tests. Signed-off-by: krishna2323 --- .../Attachments/AttachmentCarousel/extractAttachments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 5ebaf8af673e..620b7ce4c01d 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -99,7 +99,7 @@ function extractAttachments( const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; actions.forEach((action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action),) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action)) { return; } From 16ca72b963baaf7deda1e8d37a23294b13eb8a16 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:10:41 +0530 Subject: [PATCH 072/296] fix: Unchanged files with check annotations. Signed-off-by: krishna2323 --- src/libs/ReportUtils.ts | 4 ++-- src/libs/SidebarUtils.ts | 2 +- src/libs/actions/Report.ts | 10 ++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7ad1b72dfebd..7b2d592057d1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8233,7 +8233,7 @@ function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry { * @param actionsToMerge * @returns containing the calculated message preview data of the report */ -function getReportLastMessage(reportID: string, actionsToMerge?: ReportActions) { +function getReportLastMessage(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge?: ReportActions) { let result: Partial = { lastMessageTranslationKey: '', lastMessageText: '', @@ -8243,7 +8243,7 @@ function getReportLastMessage(reportID: string, actionsToMerge?: ReportActions) const {lastMessageText = '', lastMessageTranslationKey = ''} = getLastVisibleMessage(reportID, actionsToMerge); if (lastMessageText || lastMessageTranslationKey) { - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, actionsToMerge); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); const lastVisibleActionCreated = lastVisibleAction?.created; const lastActorAccountID = lastVisibleAction?.actorAccountID; result = { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 862750920bbc..d88698e7647a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -485,7 +485,7 @@ function getOptionData({ result.alternateText = lastMessageTextFromReport.length > 0 ? ReportUtils.formatReportLastMessageText(Parser.htmlToText(lastMessageText)) - : ReportActionsUtils.getLastVisibleMessage(report.reportID, {}, lastAction)?.lastMessageText; + : ReportActionsUtils.getLastVisibleMessage(report.reportID, result.isAllowedToComment, {}, lastAction)?.lastMessageText; if (!result.alternateText) { result.alternateText = ReportUtils.formatReportLastMessageText(getWelcomeMessage(report, policy).messageText ?? Localize.translateLocal('report.noActivityYet')); } diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 03ad1de54b5d..20bbaf404190 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3913,9 +3913,10 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }, }; - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, lastMessageText: report?.lastMessageText, @@ -3988,9 +3989,10 @@ function resolveActionableReportMentionWhisper( }, }; - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); - const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, lastMessageText: report?.lastMessageText, From 95bec1fc21766cb15edd356762c84936782092c4 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:16:54 +0530 Subject: [PATCH 073/296] minor fixes. Signed-off-by: krishna2323 --- src/libs/ReportUtils.ts | 10 ++++++---- src/libs/actions/Report.ts | 6 ++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7b2d592057d1..a466561e2d83 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2609,7 +2609,7 @@ function buildOptimisticCancelPaymentReportAction(expenseReportID: string, amoun */ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: ReportActions = {}): LastVisibleMessage { const report = getReportOrDraftReport(reportID); - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '-1', actionsToMerge); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge); // For Chat Report with deleted parent actions, let us fetch the correct message if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && !isEmptyObject(report) && isChatReport(report)) { @@ -2620,7 +2620,7 @@ function getLastVisibleMessage(reportID: string | undefined, actionsToMerge: Rep } // Fetch the last visible message for report represented by reportID and based on actions to merge. - return ReportActionsUtils.getLastVisibleMessage(reportID ?? '-1', actionsToMerge); + return ReportActionsUtils.getLastVisibleMessage(reportID ?? '-1', canUserPerformWriteAction(report), actionsToMerge); } /** @@ -8231,9 +8231,10 @@ function findPolicyExpenseChatByPolicyID(policyID: string): OnyxEntry { * A function to get the report last message. This is usually used to restore the report message preview in LHN after report actions change. * @param reportID * @param actionsToMerge + * @param canUserPerformWriteActionInReport * @returns containing the calculated message preview data of the report */ -function getReportLastMessage(reportID: string, canUserPerformWriteAction?: boolean, actionsToMerge?: ReportActions) { +function getReportLastMessage(reportID: string, actionsToMerge?: ReportActions) { let result: Partial = { lastMessageTranslationKey: '', lastMessageText: '', @@ -8243,7 +8244,8 @@ function getReportLastMessage(reportID: string, canUserPerformWriteAction?: bool const {lastMessageText = '', lastMessageTranslationKey = ''} = getLastVisibleMessage(reportID, actionsToMerge); if (lastMessageText || lastMessageTranslationKey) { - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); + const report = getReportOrDraftReport(reportID); + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, canUserPerformWriteAction(report), actionsToMerge); const lastVisibleActionCreated = lastVisibleAction?.created; const lastActorAccountID = lastVisibleAction?.actorAccountID; result = { diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 20bbaf404190..58b4be53e2f0 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -3914,8 +3914,7 @@ function resolveActionableMentionWhisper(reportId: string, reportAction: OnyxEnt }; const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; - const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, @@ -3990,8 +3989,7 @@ function resolveActionableReportMentionWhisper( }; const report = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportId}`]; - const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); - const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, canUserPerformWriteAction, optimisticReportActions as ReportActions); + const reportUpdateDataWithPreviousLastMessage = ReportUtils.getReportLastMessage(reportId, optimisticReportActions as ReportActions); const reportUpdateDataWithCurrentLastMessage = { lastMessageTranslationKey: report?.lastMessageTranslationKey, From 0ac89334bd30ba159603543da120676bbf158781 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:27:05 +0530 Subject: [PATCH 074/296] fix: TypeScript Checks. Signed-off-by: krishna2323 --- src/libs/actions/IOU.ts | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7ce9b9dfb272..fc16084f91fb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -1734,8 +1734,12 @@ function getDeleteTrackExpenseInformation( }, ...(actionableWhisperReportActionID && {[actionableWhisperReportActionID]: {originalMessage: {resolution}}}), } as OnyxTypes.ReportActions; - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(chatReport?.reportID ?? '-1', updatedReportAction); - const {lastMessageText = '', lastMessageHtml = ''} = ReportActionsUtils.getLastVisibleMessage(chatReport?.reportID ?? '-1', updatedReportAction); + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(chatReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction); + const {lastMessageText = '', lastMessageHtml = ''} = ReportActionsUtils.getLastVisibleMessage(chatReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction); // STEP 4: Build Onyx data const optimisticData: OnyxUpdate[] = []; @@ -5705,8 +5709,12 @@ function prepareToCleanUpMoneyRequest(transactionID: string, reportAction: OnyxT }, } as Record>; - const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1', updatedReportAction); - const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1', updatedReportAction).lastMessageText; + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } + const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(iouReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction); + const iouReportLastMessageText = ReportActionsUtils.getLastVisibleMessage(iouReport?.reportID ?? '-1', canUserPerformWriteAction, updatedReportAction).lastMessageText; const shouldDeleteIOUReport = iouReportLastMessageText.length === 0 && !ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && (!transactionThreadID || shouldDeleteTransactionThread); @@ -5899,6 +5907,10 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo } if (shouldDeleteIOUReport) { + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } onyxUpdates.push( { onyxMethod: Onyx.METHOD.MERGE, @@ -5906,8 +5918,12 @@ function cleanUpMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repo value: { hasOutstandingChildRequest: false, iouReportID: null, - lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.lastMessageText, - lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.created, + lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, { + [reportPreviewAction?.reportActionID ?? '-1']: null, + })?.lastMessageText, + lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, { + [reportPreviewAction?.reportActionID ?? '-1']: null, + })?.created, }, }, { @@ -6015,14 +6031,21 @@ function deleteMoneyRequest(transactionID: string, reportAction: OnyxTypes.Repor } if (shouldDeleteIOUReport) { + let canUserPerformWriteAction = true; + if (chatReport) { + canUserPerformWriteAction = !!ReportUtils.canUserPerformWriteAction(chatReport); + } optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport?.reportID}`, value: { hasOutstandingChildRequest: false, iouReportID: null, - lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.lastMessageText, - lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', {[reportPreviewAction?.reportActionID ?? '-1']: null})?.created, + lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, {[reportPreviewAction?.reportActionID ?? '-1']: null}) + ?.lastMessageText, + lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport?.chatReportID ?? '-1', canUserPerformWriteAction, { + [reportPreviewAction?.reportActionID ?? '-1']: null, + })?.created, }, }); optimisticData.push({ From 5fb19370ad8426a34892bf2d8a0d60b8d3b032fa Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 31 Oct 2024 05:36:29 +0530 Subject: [PATCH 075/296] fix: TypeScript Checks. Signed-off-by: krishna2323 --- src/libs/ReportActionsUtils.ts | 2 +- src/libs/actions/Task.ts | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7778cb29d552..bd04628ea264 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1100,7 +1100,7 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn * When we delete certain reports, we want to check whether there are any visible actions left to display. * If there are no visible actions left (including system messages), we can hide the report from view entirely */ -function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteAction: boolean, actionsToMerge: ReportActions = {}): boolean { +function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteAction?: boolean, 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, reportID, canUserPerformWriteAction)); diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index c5a2442048fc..130023be885b 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -967,9 +967,10 @@ function deleteTask(report: OnyxEntry) { const optimisticReportActionID = optimisticCancelReportAction.reportActionID; const parentReportAction = getParentReportAction(report); const parentReport = getParentReport(report); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // If the task report is the last visible action in the parent report, we should navigate back to the parent report - const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID ?? '-1'); + const shouldDeleteTaskReport = !ReportActionsUtils.doesReportHaveVisibleActions(report.reportID ?? '-1', canUserPerformWriteAction); const optimisticReportAction: Partial = { pendingAction: shouldDeleteTaskReport ? CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE : CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, previousMessage: parentReportAction?.message, @@ -1006,8 +1007,14 @@ function deleteTask(report: OnyxEntry) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport?.reportID}`, value: { - lastMessageText: ReportActionsUtils.getLastVisibleMessage(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions).lastMessageText ?? '', - lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(parentReport?.reportID ?? '-1', optimisticReportActions as OnyxTypes.ReportActions)?.created, + lastMessageText: + ReportActionsUtils.getLastVisibleMessage(parentReport?.reportID ?? '-1', canUserPerformWriteAction, optimisticReportActions as OnyxTypes.ReportActions).lastMessageText ?? + '', + lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction( + parentReport?.reportID ?? '-1', + canUserPerformWriteAction, + optimisticReportActions as OnyxTypes.ReportActions, + )?.created, hasOutstandingChildTask, }, }, From 3ff8ba053cc4f2c732f93617e7f26d2f462b955e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 01:13:20 +0530 Subject: [PATCH 076/296] feat: animation after approving an expense. Signed-off-by: krishna2323 --- src/components/ProcessMoneyReportHoldMenu.tsx | 3 +++ .../ReportActionItem/ReportPreview.tsx | 23 +++++++++++++--- .../AnimatedSettlementButton.tsx | 27 +++++++++++++++---- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/components/ProcessMoneyReportHoldMenu.tsx b/src/components/ProcessMoneyReportHoldMenu.tsx index 3d6ad9006dc5..ba320a594135 100644 --- a/src/components/ProcessMoneyReportHoldMenu.tsx +++ b/src/components/ProcessMoneyReportHoldMenu.tsx @@ -66,6 +66,9 @@ function ProcessMoneyReportHoldMenu({ const onSubmit = (full: boolean) => { if (isApprove) { + if (startAnimation) { + startAnimation(); + } IOU.approveMoneyRequest(moneyRequestReport, full); if (!full && isLinkedTransactionHeld(Navigation.getTopmostReportActionId() ?? '-1', moneyRequestReport?.reportID ?? '')) { Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(moneyRequestReport?.reportID ?? '')); diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index dc4e396ee75e..c2be6db0a4aa 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -120,6 +120,7 @@ function ReportPreview({ ); const [isPaidAnimationRunning, setIsPaidAnimationRunning] = useState(false); + const [isApprovedAnimationRunning, setIsApprovedAnimationRunning] = useState(false); const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [requestType, setRequestType] = useState(); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); @@ -200,11 +201,18 @@ function ReportPreview({ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const stopAnimation = useCallback(() => setIsPaidAnimationRunning(false), []); + const stopAnimation = useCallback(() => { + setIsPaidAnimationRunning(false); + setIsApprovedAnimationRunning(false); + }, []); const startAnimation = useCallback(() => { setIsPaidAnimationRunning(true); HapticFeedback.longPress(); }, []); + const startApprovedAnimation = useCallback(() => { + setIsApprovedAnimationRunning(true); + HapticFeedback.longPress(); + }, []); const confirmPayment = useCallback( (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { @@ -236,6 +244,8 @@ function ReportPreview({ } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { setIsHoldMenuVisible(true); } else { + setIsApprovedAnimationRunning(true); + HapticFeedback.longPress(); IOU.approveMoneyRequest(iouReport, true); } }; @@ -427,7 +437,7 @@ function ReportPreview({ const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(iouReport); useEffect(() => { - if (!isPaidAnimationRunning) { + if (!isPaidAnimationRunning || isApprovedAnimationRunning) { return; } @@ -556,6 +566,7 @@ function ReportPreview({ { + if (requestType === 'approve') { + startApprovedAnimation(); + } else { + startAnimation(); + } + }} /> )} diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 5de528d741a2..375e76a33582 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -11,9 +11,10 @@ import type SettlementButtonProps from './types'; type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; + isApprovedAnimationRunning: boolean; }; -function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const buttonScale = useSharedValue(1); @@ -38,7 +39,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is overflow: 'hidden', marginTop: buttonMarginTop.value, })); - const buttonDisabledStyle = isPaidAnimationRunning + const buttonDisabledStyle = isApprovedAnimationRunning ? { opacity: 1, ...styles.cursorDefault, @@ -56,7 +57,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]); useEffect(() => { - if (!isPaidAnimationRunning) { + if (!isApprovedAnimationRunning && !isPaidAnimationRunning) { resetAnimation(); return; } @@ -73,7 +74,18 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is ); buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); - }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]); + }, [ + isPaidAnimationRunning, + isApprovedAnimationRunning, + onAnimationFinish, + buttonOpacity, + buttonScale, + height, + paymentCompleteTextOpacity, + paymentCompleteTextScale, + buttonMarginTop, + resetAnimation, + ]); return ( @@ -82,11 +94,16 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is {translate('iou.paymentComplete')} )} + {isApprovedAnimationRunning && ( + + {translate('iou.approved')} + + )} From 31ddb55edabc6b07126cd908af2631fc63633e8e Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 01:38:27 +0530 Subject: [PATCH 077/296] minor fixes. Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 6 +++- .../AnimatedSettlementButton.tsx | 34 ++++++++++++++----- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index c2be6db0a4aa..27068ff2f80f 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -203,6 +203,9 @@ function ReportPreview({ const stopAnimation = useCallback(() => { setIsPaidAnimationRunning(false); + }, []); + + const stopApprovedAnimation = useCallback(() => { setIsApprovedAnimationRunning(false); }, []); const startAnimation = useCallback(() => { @@ -567,6 +570,7 @@ function ReportPreview({ onlyShowPayElsewhere={onlyShowPayElsewhere} isPaidAnimationRunning={isPaidAnimationRunning} isApprovedAnimationRunning={isApprovedAnimationRunning} + onApprovedAnimationFinish={stopApprovedAnimation} onAnimationFinish={stopAnimation} formattedAmount={getSettlementAmount() ?? ''} currency={iouReport?.currency} @@ -636,7 +640,7 @@ function ReportPreview({ moneyRequestReport={iouReport} transactionCount={numberOfRequests} startAnimation={() => { - if (requestType === 'approve') { + if (requestType === CONST.IOU.REPORT_ACTION_TYPE.APPROVE) { startApprovedAnimation(); } else { startAnimation(); diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 375e76a33582..f8205a1b1ab0 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -12,9 +12,17 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; isApprovedAnimationRunning: boolean; + onApprovedAnimationFinish: () => void; }; -function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({ + isPaidAnimationRunning, + onAnimationFinish, + isApprovedAnimationRunning, + onApprovedAnimationFinish, + isDisabled, + ...settlementButtonProps +}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const buttonScale = useSharedValue(1); @@ -39,12 +47,13 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is overflow: 'hidden', marginTop: buttonMarginTop.value, })); - const buttonDisabledStyle = isApprovedAnimationRunning - ? { - opacity: 1, - ...styles.cursorDefault, - } - : undefined; + const buttonDisabledStyle = + isPaidAnimationRunning || isApprovedAnimationRunning + ? { + opacity: 1, + ...styles.cursorDefault, + } + : undefined; const resetAnimation = useCallback(() => { // eslint-disable-next-line react-compiler/react-compiler @@ -70,7 +79,15 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY; height.value = withDelay( totalDelay, - withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), + withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => + runOnJS(() => { + if (isApprovedAnimationRunning) { + onApprovedAnimationFinish(); + } else { + onAnimationFinish(); + } + })(), + ), ); buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); @@ -85,6 +102,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is paymentCompleteTextScale, buttonMarginTop, resetAnimation, + onApprovedAnimationFinish, ]); return ( From a8154c46aaadca4f78d3bdf1747177ef4927eea9 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Sat, 2 Nov 2024 14:31:12 +0530 Subject: [PATCH 078/296] feat: Update Default / Custom Workspace Invite Behavior. Signed-off-by: krishna2323 --- .../workspace/WorkspaceInviteMessagePage.tsx | 16 ++++++++-------- src/pages/workspace/WorkspaceInvitePage.tsx | 2 ++ src/pages/workspace/WorkspaceMembersPage.tsx | 2 ++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index 89cab963fb43..f0317284e8f9 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {Keyboard, View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; @@ -20,6 +19,7 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useViewportOffsetTop from '@hooks/useViewportOffsetTop'; +import * as FormActions from '@libs/actions/FormActions'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import Parser from '@libs/Parser'; @@ -48,6 +48,7 @@ type WorkspaceInviteMessagePageProps = WithPolicyAndFullscreenLoadingProps & function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: WorkspaceInviteMessagePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [formData] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM_DRAFT); const viewportOffsetTop = useViewportOffsetTop(); const [welcomeNote, setWelcomeNote] = useState(); @@ -66,6 +67,8 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const getDefaultWelcomeNote = useCallback(() => { return ( + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + formData?.[INPUT_IDS.WELCOME_MESSAGE] || // workspaceInviteMessageDraft can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing workspaceInviteMessageDraft || @@ -76,7 +79,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: workspaceName: policy?.name ?? '', }) ); - }, [workspaceInviteMessageDraft, policy, translate]); + }, [workspaceInviteMessageDraft, policy, translate, formData]); useEffect(() => { if (isOnyxLoading) { @@ -93,16 +96,13 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [isOnyxLoading]); - const debouncedSaveDraft = lodashDebounce((newDraft: string | null) => { - Policy.setWorkspaceInviteMessageDraft(route.params.policyID, newDraft); - }); - const sendInvitation = () => { Keyboard.dismiss(); const policyMemberAccountIDs = Object.values(PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); // Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details Member.addMembersToWorkspace(invitedEmailsToAccountIDsDraft ?? {}, `${welcomeNoteSubject}\n\n${welcomeNote}`, route.params.policyID, policyMemberAccountIDs); - debouncedSaveDraft(null); + Policy.setWorkspaceInviteMessageDraft(route.params.policyID, welcomeNote ?? null); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); Navigation.dismissModal(); }; @@ -194,7 +194,6 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: value={welcomeNote} onChangeText={(text: string) => { setWelcomeNote(text); - debouncedSaveDraft(text); }} ref={(element: AnimatedTextInputRef) => { if (!element) { @@ -205,6 +204,7 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: } inputCallbackRef(element); }} + shouldSaveDraft /> diff --git a/src/pages/workspace/WorkspaceInvitePage.tsx b/src/pages/workspace/WorkspaceInvitePage.tsx index ad48d15aa9df..bfa13ef3f65d 100644 --- a/src/pages/workspace/WorkspaceInvitePage.tsx +++ b/src/pages/workspace/WorkspaceInvitePage.tsx @@ -16,6 +16,7 @@ import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as FormActions from '@libs/actions/FormActions'; import * as ReportActions from '@libs/actions/Report'; import {READ_COMMANDS} from '@libs/API/types'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; @@ -78,6 +79,7 @@ function WorkspaceInvitePage({route, betas, invitedEmailsToAccountIDsDraft, poli useEffect(() => { return () => { Member.setWorkspaceInviteMembersDraft(route.params.policyID, {}); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); }; // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [route.params.policyID]); diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 96b6d31e5a2e..cb914591a59d 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -30,6 +30,7 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as FormActions from '@libs/actions/FormActions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; @@ -417,6 +418,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const invitedEmails = Object.values(invitedEmailsToAccountIDsDraft).map(String); selectionListRef.current?.scrollAndHighlightItem?.(invitedEmails, 1500); Member.setWorkspaceInviteMembersDraft(route.params.policyID, {}); + FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); }, [invitedEmailsToAccountIDsDraft, route.params.policyID, isFocused, accountIDs, prevAccountIDs]); const getHeaderMessage = () => { From d5490b83d4059c4b940f874cd76578ac92f97f6f Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 5 Nov 2024 16:01:11 +0700 Subject: [PATCH 079/296] update isEnd to false when the video is playing --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index fad641e696ae..4a18f9abeb69 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -188,6 +188,8 @@ function BaseVideoPlayer({ [playVideo, videoResumeTryNumberRef], ); + console.log(isEnded); + const prevIsMutedRef = useRef(false); const prevVolumeRef = useRef(0); @@ -207,6 +209,8 @@ function BaseVideoPlayer({ setIsEnded(status.didJustFinish && !status.isLooping); setControlStatusState(CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW); controlsOpacity.value = 1; + } else if (status.isPlaying && isEnded) { + setIsEnded(false); } if (prevIsMutedRef.current && prevVolumeRef.current === 0 && !status.isMuted) { From 4acf40ca91ed28432d3a0e56cbd8812d22367140 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 5 Nov 2024 16:02:05 +0700 Subject: [PATCH 080/296] remove log --- src/components/VideoPlayer/BaseVideoPlayer.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx index 4a18f9abeb69..1e46c595610e 100644 --- a/src/components/VideoPlayer/BaseVideoPlayer.tsx +++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx @@ -188,8 +188,6 @@ function BaseVideoPlayer({ [playVideo, videoResumeTryNumberRef], ); - console.log(isEnded); - const prevIsMutedRef = useRef(false); const prevVolumeRef = useRef(0); From 7e6f6c14e0d5d20969e35874b4790df7c5b0c725 Mon Sep 17 00:00:00 2001 From: truph01 Date: Tue, 5 Nov 2024 20:04:21 +0700 Subject: [PATCH 081/296] fix: update trans --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index da158b15cd77..a2d230b141ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -779,7 +779,7 @@ const translations = { locationAccessMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', locationErrorTitle: 'Permitir acceso a la ubicación', locationErrorMessage: 'El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que vayas.', - allowLocationFromSetting: `Location access helps us keep your timezone and currency accurate wherever you go. Please allow location access from your device's permission settings.`, + allowLocationFromSetting: `El acceso a la ubicación nos ayuda a mantener tu zona horaria y moneda precisas dondequiera que estés. Por favor, permite el acceso a la ubicación en la configuración de permisos de tu dispositivo.`, cameraErrorMessage: 'Se ha producido un error al hacer una foto. Por favor, inténtalo de nuevo.', dropTitle: 'Suéltalo', dropMessage: 'Suelta tu archivo aquí', From 9070620537f3ca0bd022dee4c8f495a904b7f469 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 02:36:29 +0530 Subject: [PATCH 082/296] minor update. Signed-off-by: krishna2323 --- src/pages/workspace/WorkspaceInviteMessagePage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/WorkspaceInviteMessagePage.tsx b/src/pages/workspace/WorkspaceInviteMessagePage.tsx index f0317284e8f9..4b437e1ffd78 100644 --- a/src/pages/workspace/WorkspaceInviteMessagePage.tsx +++ b/src/pages/workspace/WorkspaceInviteMessagePage.tsx @@ -68,16 +68,16 @@ function WorkspaceInviteMessagePage({policy, route, currentUserPersonalDetails}: const getDefaultWelcomeNote = useCallback(() => { return ( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - formData?.[INPUT_IDS.WELCOME_MESSAGE] || + formData?.[INPUT_IDS.WELCOME_MESSAGE] ?? // workspaceInviteMessageDraft can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - workspaceInviteMessageDraft || + workspaceInviteMessageDraft ?? // policy?.description can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - Parser.htmlToMarkdown(policy?.description ?? '') || - translate('workspace.common.welcomeNote', { - workspaceName: policy?.name ?? '', - }) + (Parser.htmlToMarkdown(policy?.description ?? '') || + translate('workspace.common.welcomeNote', { + workspaceName: policy?.name ?? '', + })) ); }, [workspaceInviteMessageDraft, policy, translate, formData]); From c57030110d6b8c9ba0a9a453352c4b417f856935 Mon Sep 17 00:00:00 2001 From: Wildan Muhlis Date: Wed, 6 Nov 2024 09:00:19 +0700 Subject: [PATCH 083/296] Fix list not scrolled up when search query empty --- src/components/Search/SearchRouter/SearchRouter.tsx | 7 ++++++- src/components/SelectionList/BaseSelectionList.tsx | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 83d7d5d89b20..6f62a08db00f 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -37,6 +37,7 @@ import ROUTES from '@src/ROUTES'; import SearchRouterInput from './SearchRouterInput'; import SearchRouterList from './SearchRouterList'; import type {ItemWithQuery} from './SearchRouterList'; +import isEmpty from 'lodash/isEmpty'; type SearchRouterProps = { onRouterClose: () => void; @@ -293,6 +294,7 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { ], ); + const prevUserQueryRef = useRef(null); const onSearchChange = useCallback( (userQuery: string) => { let newUserQuery = userQuery; @@ -302,11 +304,14 @@ function SearchRouter({onRouterClose}: SearchRouterProps) { setTextInputValue(newUserQuery); const autocompleteParsedQuery = parseForAutocomplete(newUserQuery); updateAutocomplete(autocompleteParsedQuery?.autocomplete?.value ?? '', autocompleteParsedQuery?.ranges ?? [], autocompleteParsedQuery?.autocomplete?.key); - if (newUserQuery) { + if (newUserQuery || !isEmpty(prevUserQueryRef.current)) { listRef.current?.updateAndScrollToFocusedIndex(0); } else { listRef.current?.updateAndScrollToFocusedIndex(-1); } + + // Store the previous newUserQuery + prevUserQueryRef.current = newUserQuery; }, [autocompleteSuggestions, setTextInputValue, updateAutocomplete], ); diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 3e1b3a3c2d70..ffb6c64a0fc5 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -572,8 +572,9 @@ function BaseSelectionList( } // Remove the focus if the search input is empty or selected options length is changed (and allOptions length remains the same) // else focus on the first non disabled item + const newSelectedIndex = - textInputValue === '' || (flattenedSections.selectedOptions.length !== prevSelectedOptionsLength && prevAllOptionsLength === flattenedSections.allOptions.length) ? -1 : 0; + (isEmpty(prevTextInputValue) && textInputValue === '') || (flattenedSections.selectedOptions.length !== prevSelectedOptionsLength && prevAllOptionsLength === flattenedSections.allOptions.length) ? -1 : 0; // reseting the currrent page to 1 when the user types something setCurrentPage(1); From 88cd7752bfb26bf72e61ab75e639d7650534b042 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 12:16:50 +0530 Subject: [PATCH 084/296] fix height when approve button changes to pay. Signed-off-by: krishna2323 --- .../ReportActionItem/ReportPreview.tsx | 11 ++++----- .../AnimatedSettlementButton.tsx | 23 ++++++++----------- src/libs/actions/IOU.ts | 3 ++- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 27068ff2f80f..cbb50243dfe5 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -203,11 +203,9 @@ function ReportPreview({ const stopAnimation = useCallback(() => { setIsPaidAnimationRunning(false); - }, []); - - const stopApprovedAnimation = useCallback(() => { setIsApprovedAnimationRunning(false); }, []); + const startAnimation = useCallback(() => { setIsPaidAnimationRunning(true); HapticFeedback.longPress(); @@ -346,14 +344,15 @@ function ReportPreview({ const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const getCanIOUBePaid = useCallback( - (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere), + (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions, onlyShowPayElsewhere, shouldCheckApprovedState), [iouReport, chatReport, policy, allTransactions], ); const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]); + const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]); const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]); const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere; - const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]); + const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning; const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport); @@ -570,7 +569,7 @@ function ReportPreview({ onlyShowPayElsewhere={onlyShowPayElsewhere} isPaidAnimationRunning={isPaidAnimationRunning} isApprovedAnimationRunning={isApprovedAnimationRunning} - onApprovedAnimationFinish={stopApprovedAnimation} + canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning} onAnimationFinish={stopAnimation} formattedAmount={getSettlementAmount() ?? ''} currency={iouReport?.currency} diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index f8205a1b1ab0..7e42c8cdc45c 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -12,15 +12,15 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & { isPaidAnimationRunning: boolean; onAnimationFinish: () => void; isApprovedAnimationRunning: boolean; - onApprovedAnimationFinish: () => void; + canIOUBePaid: boolean; }; function AnimatedSettlementButton({ isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, - onApprovedAnimationFinish, isDisabled, + canIOUBePaid, ...settlementButtonProps }: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); @@ -77,19 +77,15 @@ function AnimatedSettlementButton({ // Wait for the above animation + 1s delay before hiding the component const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY; + const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning; height.value = withDelay( totalDelay, - withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => - runOnJS(() => { - if (isApprovedAnimationRunning) { - onApprovedAnimationFinish(); - } else { - onAnimationFinish(); - } - })(), - ), + withTiming(willShowPaymentButton ? variables.componentSizeNormal : 0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), + ); + buttonMarginTop.value = withDelay( + totalDelay, + withTiming(willShowPaymentButton ? styles.expenseAndReportPreviewTextButtonContainer.gap : 0, {duration: CONST.ANIMATION_PAID_DURATION}), ); - buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); }, [ isPaidAnimationRunning, @@ -102,7 +98,8 @@ function AnimatedSettlementButton({ paymentCompleteTextScale, buttonMarginTop, resetAnimation, - onApprovedAnimationFinish, + canIOUBePaid, + styles.expenseAndReportPreviewTextButtonContainer.gap, ]); return ( diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7a72df9f1d87..6fafe3c59a4f 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7060,6 +7060,7 @@ function canIOUBePaid( policy: OnyxTypes.OnyxInputOrEntry, transactions?: OnyxTypes.Transaction[], onlyShowPayElsewhere = false, + shouldCheckApprovedState = true, ) { const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); const reportNameValuePairs = ReportUtils.getReportNameValuePairs(chatReport?.reportID); @@ -7113,7 +7114,7 @@ function canIOUBePaid( reimbursableSpend !== 0 && !isChatReportArchived && !isAutoReimbursable && - !shouldBeApproved && + (!shouldBeApproved || !shouldCheckApprovedState) && !isPayAtEndExpenseReport ); } From ab94ee9076b5708e2deb91e6bff5e9da38cb00de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 6 Nov 2024 08:55:25 +0000 Subject: [PATCH 085/296] Change guidelines to forbid defaulting values --- contributingGuides/STYLE.md | 57 ++++++++++++++++++++++++++++++++----- src/CONST.ts | 7 +++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index e6660d848129..755f5228a8a7 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -477,20 +477,63 @@ if (ref.current && 'getBoundingClientRect' in ref.current) { ### Default value for inexistent IDs - Use `'-1'` or `-1` when there is a possibility that the ID property of an Onyx value could be `null` or `undefined`. +Use `CONST.DEFAULT_NUMBER_ID` when there is a possibility that the number ID property of an Onyx value could be `null` or `undefined`. **Do not default string IDs to any value unless absolutely necessary**, in case it's necessary use `CONST.DEFAULT_STRING_ID` instead. ``` ts // BAD -const foo = report?.reportID ?? ''; -const bar = report?.reportID ?? '0'; +const accountID = report?.ownerAccountID ?? -1; +const accountID = report?.ownerAccountID ?? 0; +const reportID = report?.reportID ?? '-1'; -report ? report.reportID : '0'; -report ? report.reportID : ''; +// BAD +report ? report.ownerAccountID : -1; + +// GOOD +const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; +const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; +const reportID = report?.reportID; // GOOD -const foo = report?.reportID ?? '-1'; +report ? report.ownerAccountID : CONST.DEFAULT_NUMBER_ID; +``` + +Here are some common cases you may face when fixing your code to remove the default values. + +#### **Case 1**: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. + +```diff +-Report.getNewerActions(newestActionCurrentReport?.reportID ?? '-1', newestActionCurrentReport?.reportActionID ?? '-1'); ++Report.getNewerActions(newestActionCurrentReport?.reportID, newestActionCurrentReport?.reportActionID); +``` + +> error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. Type 'undefined' is not assignable to type 'string'. + +We need to change `Report.getNewerActions()` arguments to allow `undefined`. By doing that we could add a condition that return early if one of the parameters are falsy, preventing the code (which is expecting defined IDs) from executing. + +```diff +-function getNewerActions(reportID: string, reportActionID: string) { ++function getNewerActions(reportID: string | undefined, reportActionID: string | undefined) { ++ if (!reportID || !reportActionID) { ++ return; ++ } +``` + +#### **Case 2**: Type 'undefined' cannot be used as an index type. + +```diff +function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { +- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; ++ const parentReportAction = parentReportActions?.[report?.parentReportActionID]; +``` + +> error TS2538: Type 'undefined' cannot be used as an index type. + +This error is inside a component, so we can't just make conditions with early returns here. We can instead use `String(report?.parentReportActionID)` to try to convert the value to `string`. If the value is `undefined` the result string will be `'undefined'`, which will be used to find a record inside `parentReportActions` and, same as `-1`, would find nothing. -report ? report.reportID : '-1'; +```diff +function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { +- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; ++ const parentReportAction = parentReportActions?.[String(report?.parentReportActionID)]; ``` ### Extract complex types diff --git a/src/CONST.ts b/src/CONST.ts index ddf9ebad5b66..89276c51dd13 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -16,6 +16,11 @@ import type PlaidBankAccount from './types/onyx/PlaidBankAccount'; const EMPTY_ARRAY = Object.freeze([]); const EMPTY_OBJECT = Object.freeze({}); +const DEFAULT_NUMBER_ID = 0; + +/** Only default a string ID to this value if absolutely necessary! */ +const DEFAULT_STRING_ID = ''; + const CLOUDFRONT_DOMAIN = 'cloudfront.net'; const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`; const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(Config?.NEW_EXPENSIFY_URL ?? 'https://new.expensify.com'); @@ -833,6 +838,8 @@ const CONST = { CLOUDFRONT_URL, EMPTY_ARRAY, EMPTY_OBJECT, + DEFAULT_NUMBER_ID, + DEFAULT_STRING_ID, USE_EXPENSIFY_URL, GOOGLE_MEET_URL_ANDROID: 'https://meet.google.com', GOOGLE_DOC_IMAGE_LINK_MATCH: 'googleusercontent.com', From edf7c52fb9464d18297e71e69cb55f4dadc01f1f Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Wed, 6 Nov 2024 17:42:07 +0530 Subject: [PATCH 086/296] remove reportID parameter from shouldReportActionBeVisible. Signed-off-by: krishna2323 --- .../AttachmentCarousel/extractAttachments.ts | 2 +- .../LHNOptionsList/LHNOptionsList.tsx | 3 ++- src/components/ParentNavigationSubtitle.tsx | 6 +++-- src/hooks/usePaginatedReportActions.ts | 2 +- src/libs/OptionsListUtils.ts | 4 +-- src/libs/Permissions.ts | 1 + src/libs/ReportActionsUtils.ts | 27 ++++++++++--------- src/libs/SidebarUtils.ts | 6 +++-- src/libs/SubscriptionUtils.ts | 1 + src/libs/actions/Report.ts | 2 +- src/pages/Debug/Report/DebugReportActions.tsx | 4 +-- src/pages/home/ReportScreen.tsx | 4 +-- .../report/ReportActionItemParentAction.tsx | 1 - src/pages/home/report/ReportActionsList.tsx | 2 +- src/pages/home/report/ReportActionsView.tsx | 2 +- src/pages/home/report/ThreadDivider.tsx | 1 - .../perf-test/ReportActionsUtils.perf-test.ts | 4 +-- tests/unit/ReportActionsUtilsTest.ts | 6 ++--- 18 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 620b7ce4c01d..51d5cfb1d981 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -99,7 +99,7 @@ function extractAttachments( const actions = [...(parentReportAction ? [parentReportAction] : []), ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; actions.forEach((action, key) => { - if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, reportID, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action)) { + if (!ReportActionsUtils.shouldReportActionBeVisible(action, key, canUserPerformWriteAction) || ReportActionsUtils.isMoneyRequestAction(action)) { return; } diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index c03d7822f9e3..9594e6ede24a 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -141,7 +141,8 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const hasDraftComment = DraftCommentUtils.isValidDraftComment(draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`]); - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, reportID, ReportUtils.canUserPerformWriteAction(itemFullReport)); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(itemFullReport); + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions, canUserPerformWriteAction); const lastReportAction = sortedReportActions.at(0); // Get the transaction for the last report action diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index 248eaec9c9b1..2b808432c6ab 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -8,6 +9,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback'; import Text from './Text'; @@ -30,7 +32,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct const {workspaceName, reportName} = parentNavigationSubtitleData; const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const report = ReportUtils.getReport(parentReportID); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`); const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); // We should not display the parent navigation subtitle if the user does not have access to the parent chat (the reportName is empty in this case) @@ -42,7 +44,7 @@ function ParentNavigationSubtitle({parentNavigationSubtitleData, parentReportAct { const parentAction = ReportActionsUtils.getReportAction(parentReportID, parentReportActionID ?? '-1'); - const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', parentReportID, canUserPerformWriteAction); + const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? '-1', canUserPerformWriteAction); // 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/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index 6b476db46d7d..8bcc7c9ce539 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -17,7 +17,7 @@ function usePaginatedReportActions(reportID?: string, reportActionID?: string) { const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDWithDefault}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID ?? '-1', canUserPerformWriteAction, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, canUserPerformWriteAction, true), }); const [reportActionPages] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_PAGES}${reportIDWithDefault}`); diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 0a92e482df7a..069db8e3419f 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -335,7 +335,7 @@ Onyx.connect({ // does not match a closed or created state. const reportActionsForDisplay = sortedReportActions.filter( (reportAction, actionKey) => - ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, reportID, canUserPerformWriteAction) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, actionKey, canUserPerformWriteAction) && !ReportActionUtils.isWhisperAction(reportAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && @@ -593,7 +593,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, reportID, ReportUtils.canUserPerformWriteAction(report)) && + ReportActionUtils.shouldReportActionBeVisible(reportAction, key, ReportUtils.canUserPerformWriteAction(report)) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && ReportActionUtils.isMoneyRequestAction(reportAction), ); diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 0853bd9c18ce..7083f04fab93 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,6 +4,7 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { + return true; return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fd45d9843178..7118bf8445e9 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -635,8 +635,11 @@ 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, reportID: string, canUserPerformWriteAction?: boolean): boolean { - if ((isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPending(reportID ?? '-1') || isActionableMentionWhisper(reportAction)) && !canUserPerformWriteAction) { +function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string | number, canUserPerformWriteAction?: boolean): boolean { + if ( + (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPendingReportAction(reportAction) || isActionableMentionWhisper(reportAction)) && + !canUserPerformWriteAction + ) { return false; } @@ -715,7 +718,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, reportID: string, canUserPerformWriteAction?: boolean): boolean { +function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry, canUserPerformWriteAction?: boolean): boolean { if (!reportAction) { return false; } @@ -727,7 +730,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, reportID, canUserPerformWriteAction) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction) && !(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) && !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction)) && !isResolvedActionTrackExpense(reportAction) @@ -769,7 +772,7 @@ function getLastVisibleAction(reportID: string, canUserPerformWriteAction?: bool } else { reportActions = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}); } - const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, reportID, canUserPerformWriteAction)); + const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction)); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -838,7 +841,6 @@ function filterOutDeprecatedReportActions(reportActions: OnyxEntry | ReportAction[], - reportID: string, canUserPerformWriteAction?: boolean, shouldIncludeInvisibleActions = false, ): ReportAction[] { @@ -851,7 +853,7 @@ function getSortedReportActionsForDisplay( filteredReportActions = Object.values(reportActions).filter(Boolean); } else { filteredReportActions = Object.entries(reportActions) - .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, reportID, canUserPerformWriteAction)) + .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction)) .map(([, reportAction]) => reportAction); } @@ -1102,7 +1104,7 @@ function getOneTransactionThreadReportID(reportID: string, reportActions: OnyxEn */ function doesReportHaveVisibleActions(reportID: string, canUserPerformWriteAction?: boolean, 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, reportID, canUserPerformWriteAction)); + const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction)); // Exclude the task system message and the created message const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action)); @@ -1495,11 +1497,12 @@ function isActionableJoinRequest(reportAction: OnyxEntry): reportA return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST); } -function getActionableJoinRequestPendingReportAction(reportID: string): OnyxEntry { - const findPendingRequest = Object.values(getAllReportActions(reportID)).find( - (reportActionItem) => isActionableJoinRequest(reportActionItem) && getOriginalMessage(reportActionItem)?.choice === ('' as JoinWorkspaceResolution), - ); +function isActionableJoinRequestPendingReportAction(reportAction: OnyxEntry): boolean { + return isActionableJoinRequest(reportAction) && getOriginalMessage(reportAction)?.choice === ('' as JoinWorkspaceResolution); +} +function getActionableJoinRequestPendingReportAction(reportID: string): OnyxEntry { + const findPendingRequest = Object.values(getAllReportActions(reportID)).find((reportActionItem) => isActionableJoinRequestPendingReportAction(reportActionItem)); return findPendingRequest; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 49ed7392cec9..cbe7027e5939 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -41,13 +41,15 @@ Onyx.connect({ return; } const reportID = CollectionUtils.extractCollectionItemID(key); - + const report = ReportUtils.getReport(reportID); + const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); const actionsArray: ReportAction[] = ReportActionsUtils.getSortedReportActions(Object.values(actions)); // 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, reportID) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, + (reportAction) => + ReportActionsUtils.shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, ); const reportAction = reportActionsForDisplay.at(-1); diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index f2ceef9069fa..6cbf6d5b0d9e 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -435,6 +435,7 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { + return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 63e578a6c513..1ec8375415d4 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -298,7 +298,7 @@ registerPaginationConfig({ sortItems: (reportActions, reportID) => { const report = ReportUtils.getReport(reportID); const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); - return ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, reportID, canUserPerformWriteAction, true); + return ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, canUserPerformWriteAction, true); }, getItemID: (reportAction) => reportAction.reportActionID, }); diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index f9f0a6e9a89c..9368ca5116bd 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -22,11 +22,11 @@ type DebugReportActionsProps = { function DebugReportActions({reportID}: DebugReportActionsProps) { const {translate, datetimeToCalendarTime} = useLocalize(); const styles = useThemeStyles(); - const report = ReportUtils.getReport(reportID); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const canUserPerformWriteAction = ReportUtils.canUserPerformWriteAction(report); const [sortedAllReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { canEvict: false, - selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, reportID, canUserPerformWriteAction, true), + selector: (allReportActions) => ReportActionsUtils.getSortedReportActionsForDisplay(allReportActions, canUserPerformWriteAction, true), }); const renderItem = ({item}: ListRenderItemInfo) => ( = CONST.REPORT.MIN_INITIAL_REPORT_ACTION_COUNT || isPendingActionExist || (doesCreatedActionExists() && reportActions.length > 0); const isLinkedActionDeleted = useMemo( - () => - !!linkedAction && - !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, report?.reportID ?? '-1', ReportUtils.canUserPerformWriteAction(report)), + () => !!linkedAction && !ReportActionsUtils.shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, ReportUtils.canUserPerformWriteAction(report)), [linkedAction, report], ); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); diff --git a/src/pages/home/report/ReportActionItemParentAction.tsx b/src/pages/home/report/ReportActionItemParentAction.tsx index 67e16e5b068c..d66b91436b79 100644 --- a/src/pages/home/report/ReportActionItemParentAction.tsx +++ b/src/pages/home/report/ReportActionItemParentAction.tsx @@ -137,7 +137,6 @@ function ReportActionItemParentAction({ const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1', - ancestor.report.reportID, canUserPerformWriteAction, ); // Pop the thread report screen before navigating to the chat report. diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index b8c4a92a185e..1090e09d0a6a 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -188,7 +188,7 @@ function ReportActionsList({ ReportActionsUtils.isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, report.reportID, ReportUtils.canUserPerformWriteAction(report)), + ReportActionsUtils.shouldReportActionBeVisible(reportAction, reportAction.reportActionID, ReportUtils.canUserPerformWriteAction(report)), ), [sortedReportActions, isOffline, report], ); diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index fb1f848b677c..5319b7dc72bd 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -91,7 +91,7 @@ function ReportActionsView({ const [session] = useOnyx(ONYXKEYS.SESSION); const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? -1}`, { selector: (reportActions: OnyxEntry) => - ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, report.reportID, ReportUtils.canUserPerformWriteAction(report), true), + ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, ReportUtils.canUserPerformWriteAction(report), 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 290d173d9a5c..0cf34d6f40e6 100644 --- a/src/pages/home/report/ThreadDivider.tsx +++ b/src/pages/home/report/ThreadDivider.tsx @@ -51,7 +51,6 @@ function ThreadDivider({ancestor, isLinkDisabled = false}: ThreadDividerProps) { const isVisibleAction = ReportActionsUtils.shouldReportActionBeVisible( ancestor.reportAction, ancestor.reportAction.reportActionID ?? '-1', - ancestor.report.reportID, ReportUtils.canUserPerformWriteAction(ancestor.report), ); // Pop the thread report screen before navigating to the chat report. diff --git a/tests/perf-test/ReportActionsUtils.perf-test.ts b/tests/perf-test/ReportActionsUtils.perf-test.ts index d6c573e846e8..b0ebd7c0c10a 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, reportId, true); + const reportActionsArray = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true); 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, reportId, true)); + await measureFunction(() => ReportActionsUtils.getSortedReportActionsForDisplay(reportActions, true)); }); test('[ReportActionsUtils] getLastClosedReportAction on 10k ReportActions', async () => { diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index ef74f34f6413..b0d65517d12f 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -306,7 +306,7 @@ describe('ReportActionsUtils', () => { // eslint-disable-next-line rulesdir/prefer-at const expectedOutput: ReportAction[] = [...input.slice(0, 1), ...input.slice(2), input[1]]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); expect(result).toStrictEqual(expectedOutput); }); @@ -401,7 +401,7 @@ describe('ReportActionsUtils', () => { // eslint-disable-next-line rulesdir/prefer-at const expectedOutput: ReportAction[] = [...input.slice(0, 1), ...input.slice(2, -1), input[1]]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); expect(result).toStrictEqual(expectedOutput); }); @@ -445,7 +445,7 @@ describe('ReportActionsUtils', () => { message: [{html: '', type: 'Action type', text: 'Action text'}], }, ]; - const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, '1', true); + const result = ReportActionsUtils.getSortedReportActionsForDisplay(input, true); input.pop(); expect(result).toStrictEqual(input); }); From 1fa5412855e38528899365dd600edbe5345f736d Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Wed, 6 Nov 2024 17:02:26 +0100 Subject: [PATCH 087/296] e2e: close ANR popup in case if exception happened --- tests/e2e/testRunner.ts | 3 +++ tests/e2e/utils/closeANRPopup.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 tests/e2e/utils/closeANRPopup.ts diff --git a/tests/e2e/testRunner.ts b/tests/e2e/testRunner.ts index 5485385ad8c9..8efeef85dcf5 100644 --- a/tests/e2e/testRunner.ts +++ b/tests/e2e/testRunner.ts @@ -22,6 +22,7 @@ import compare from './compare/compare'; import defaultConfig from './config'; import createServerInstance from './server'; import reversePort from './utils/androidReversePort'; +import closeANRPopup from './utils/closeANRPopup'; import installApp from './utils/installApp'; import killApp from './utils/killApp'; import launchApp from './utils/launchApp'; @@ -287,6 +288,7 @@ const runTests = async (): Promise => { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.error(`Warmup failed with error: ${e}`); + closeANRPopup(); MeasureUtils.stop('error-warmup'); server.clearAllTestDoneListeners(); @@ -314,6 +316,7 @@ const runTests = async (): Promise => { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.error(`Unexpected error during test execution: ${e}. `); MeasureUtils.stop('error'); + closeANRPopup(); server.clearAllTestDoneListeners(); errorCountRef.errorCount += 1; if (testIteration === 0 || errorCountRef.errorCount === errorCountRef.allowedExceptions) { diff --git a/tests/e2e/utils/closeANRPopup.ts b/tests/e2e/utils/closeANRPopup.ts new file mode 100644 index 000000000000..1763bc183ad4 --- /dev/null +++ b/tests/e2e/utils/closeANRPopup.ts @@ -0,0 +1,13 @@ +import execAsync from './execAsync'; +import type {PromiseWithAbort} from './execAsync'; + +const closeANRPopup = function (platform = 'android'): PromiseWithAbort { + if (platform !== 'android') { + throw new Error(`closeANRPopup() missing implementation for platform: ${platform}`); + } + + // Press "Enter" to close the ANR popup + return execAsync(`adb shell input keyevent KEYCODE_ENTER`); +}; + +export default closeANRPopup; From b5c3e3a206e22f927ae65c24acced9e2c68770dd Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 7 Nov 2024 07:23:58 +0530 Subject: [PATCH 088/296] fix: mWeb - Chat - Part of the #Admins welcome message is still in english when changing language. Signed-off-by: krishna2323 --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index ab404143d030..14541f785bda 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -660,7 +660,7 @@ const translations = { beginningOfChatHistoryDomainRoomPartTwo: ' Úsalo para chatear con colegas, compartir consejos y hacer preguntas.', beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAdminRoomPartOneParams) => `Este chat es con los administradores del espacio de trabajo ${workspaceName}.`, - beginningOfChatHistoryAdminRoomPartTwo: ' Use it to chat about workspace setup and more.', + beginningOfChatHistoryAdminRoomPartTwo: ' Úsalo para hablar sobre la configuración del espacio de trabajo y más.', beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartOneParams) => `Este chat es con todos en ${workspaceName}.`, beginningOfChatHistoryAnnounceRoomPartTwo: ` Úsalo para hablar sobre la configuración del espacio de trabajo y más.`, beginningOfChatHistoryUserRoomPartOne: 'ste chat es para todo lo relacionado con ', From ea27cc36a326419c2246eb869392cb2567ad8a07 Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 7 Nov 2024 13:15:12 +0530 Subject: [PATCH 089/296] remove redundant code. Signed-off-by: krishna2323 --- src/libs/Permissions.ts | 1 - src/libs/SubscriptionUtils.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 7083f04fab93..0853bd9c18ce 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -4,7 +4,6 @@ import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; function canUseAllBetas(betas: OnyxEntry): boolean { - return true; return !!betas?.includes(CONST.BETAS.ALL); } diff --git a/src/libs/SubscriptionUtils.ts b/src/libs/SubscriptionUtils.ts index 6cbf6d5b0d9e..f2ceef9069fa 100644 --- a/src/libs/SubscriptionUtils.ts +++ b/src/libs/SubscriptionUtils.ts @@ -435,7 +435,6 @@ function doesUserHavePaymentCardAdded(): boolean { * Whether the user's billable actions should be restricted. */ function shouldRestrictUserBillableActions(policyID: string): boolean { - return false; const currentDate = new Date(); const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; From 40b7435863a3eb15ba2f96128930ed0415ea6f9c Mon Sep 17 00:00:00 2001 From: krishna2323 Date: Thu, 7 Nov 2024 13:30:54 +0530 Subject: [PATCH 090/296] minor update. Signed-off-by: krishna2323 --- src/libs/ReportActionsUtils.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 7118bf8445e9..ccee59f64db7 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -636,6 +636,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, canUserPerformWriteAction?: boolean): boolean { + if (!reportAction) { + return false; + } + if ( (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPendingReportAction(reportAction) || isActionableMentionWhisper(reportAction)) && !canUserPerformWriteAction @@ -643,10 +647,6 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: return false; } - if (!reportAction) { - return false; - } - if (isReportActionDeprecated(reportAction, key)) { return false; } From 4963b99051f96dec01a423ae842c6a427bbee19b Mon Sep 17 00:00:00 2001 From: kirillzyusko Date: Thu, 7 Nov 2024 10:18:15 +0100 Subject: [PATCH 091/296] fix: add missing await --- tests/e2e/testRunner.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/e2e/testRunner.ts b/tests/e2e/testRunner.ts index 8efeef85dcf5..f68a3d6a9fba 100644 --- a/tests/e2e/testRunner.ts +++ b/tests/e2e/testRunner.ts @@ -288,7 +288,7 @@ const runTests = async (): Promise => { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.error(`Warmup failed with error: ${e}`); - closeANRPopup(); + await closeANRPopup(); MeasureUtils.stop('error-warmup'); server.clearAllTestDoneListeners(); @@ -312,11 +312,11 @@ const runTests = async (): Promise => { // We run each test multiple time to average out the results for (let testIteration = 0; testIteration < config.RUNS; testIteration++) { - const onError = (e: Error) => { + const onError = async (e: Error) => { // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.error(`Unexpected error during test execution: ${e}. `); MeasureUtils.stop('error'); - closeANRPopup(); + await closeANRPopup(); server.clearAllTestDoneListeners(); errorCountRef.errorCount += 1; if (testIteration === 0 || errorCountRef.errorCount === errorCountRef.allowedExceptions) { @@ -344,7 +344,7 @@ const runTests = async (): Promise => { // Run the test on the delta app: await runTestIteration(config.DELTA_APP_PACKAGE, deltaIterationText, config.BRANCH_DELTA, launchArgs); } catch (e) { - onError(e as Error); + await onError(e as Error); } } } catch (exception) { From 088f1c34b73889f2a4e9c07e4d1d77f4f0b24f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Thu, 7 Nov 2024 10:10:11 +0000 Subject: [PATCH 092/296] Address comments --- contributingGuides/STYLE.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/contributingGuides/STYLE.md b/contributingGuides/STYLE.md index 755f5228a8a7..61e848835138 100644 --- a/contributingGuides/STYLE.md +++ b/contributingGuides/STYLE.md @@ -490,14 +490,13 @@ report ? report.ownerAccountID : -1; // GOOD const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; -const accountID = report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID; const reportID = report?.reportID; // GOOD report ? report.ownerAccountID : CONST.DEFAULT_NUMBER_ID; ``` -Here are some common cases you may face when fixing your code to remove the default values. +Here are some common cases you may face when fixing your code to remove the old/bad default values. #### **Case 1**: Argument of type 'string | undefined' is not assignable to parameter of type 'string'. @@ -522,13 +521,16 @@ We need to change `Report.getNewerActions()` arguments to allow `undefined`. By ```diff function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { -- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; -+ const parentReportAction = parentReportActions?.[report?.parentReportActionID]; + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, { + canEvict: false, + }); +- const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '-1']; ++ const parentReportAction = parentReportActions?.[report?.parentReportActionID]; ``` > error TS2538: Type 'undefined' cannot be used as an index type. -This error is inside a component, so we can't just make conditions with early returns here. We can instead use `String(report?.parentReportActionID)` to try to convert the value to `string`. If the value is `undefined` the result string will be `'undefined'`, which will be used to find a record inside `parentReportActions` and, same as `-1`, would find nothing. +This error is inside a component, so we can't simply use early return conditions here. Instead, we can use `String(report?.parentReportActionID)` to try to convert the value to `string`. If the value is `undefined`, the result string will be `'undefined'`, which will be used to find a record inside `parentReportActions` and, just like `-1`, would find nothing. ```diff function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = false, updatedTransaction, isFromReviewDuplicates = false}: MoneyRequestViewProps) { From d1727aee7ea2e869e0b9a578a369b0612a9e79a9 Mon Sep 17 00:00:00 2001 From: kubabutkiewicz Date: Thu, 7 Nov 2024 15:28:05 +0100 Subject: [PATCH 093/296] fix: first batch of console errors/warnings --- desktop/main.ts | 8 +++++++ package-lock.json | 22 +++++++++++++++---- package.json | 2 +- ...ntroller+1.14.4+001+disable-android.patch} | 4 ++-- src/CONFIG.ts | 1 + .../index.native.tsx | 7 ++++-- 6 files changed, 35 insertions(+), 9 deletions(-) rename patches/{react-native-keyboard-controller+1.14.1+001+disable-android.patch => react-native-keyboard-controller+1.14.4+001+disable-android.patch} (96%) diff --git a/desktop/main.ts b/desktop/main.ts index 1221b05a8388..fcdd5b913038 100644 --- a/desktop/main.ts +++ b/desktop/main.ts @@ -25,6 +25,14 @@ const {DESKTOP_SHORTCUT_ACCELERATOR, LOCALES} = CONST; // geolocation api (window.navigator.geolocation.getCurrentPosition) to work on desktop. // Source: https://github.com/electron/electron/blob/98cd16d336f512406eee3565be1cead86514db7b/docs/api/environment-variables.md#google_api_key process.env.GOOGLE_API_KEY = CONFIG.GCP_GEOLOCATION_API_KEY; +/** + * Suppresses Content Security Policy (CSP) console warnings related to 'unsafe-eval'. + * This is required because: + * 1. Webpack utilizes eval() for module bundling + * 2. The application requires 'unsafe-eval' in CSP to function properly + * Note: CSP warnings are expected and unavoidable in this context + */ +process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = CONFIG.ELECTRON_DISABLE_SECURITY_WARNINGS; app.setName('New Expensify'); diff --git a/package-lock.json b/package-lock.json index 0497c2e625d1..62b599507242 100644 --- a/package-lock.json +++ b/package-lock.json @@ -91,7 +91,7 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.1", + "react-native-keyboard-controller": "1.14.4", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", @@ -34696,6 +34696,16 @@ "resolved": "git+ssh://git@github.com/Expensify/react-native-image-size.git#cb392140db4953a283590d7cf93b4d0461baa2a9", "integrity": "sha512-kF/8fGsKoOnjPZceipRUaM9Xg9a/aKXU2Vm5eHYEKHrRt8FP39oCbaELPTb/vUKRTu1HmEGffDFzRT02BcdzYQ==" }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.6.tgz", + "integrity": "sha512-1pHnFTlBahins6UAajXUqeCOHew9l9C2C8tErnpGC3IyLJzvxD+TpYAixnCbrVS52f7+NvMttbiSI290XfwN0w==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.2.0", + "react-native": ">=0.73.0" + } + }, "node_modules/react-native-key-command": { "version": "1.0.8", "license": "MIT", @@ -34715,9 +34725,13 @@ "license": "MIT" }, "node_modules/react-native-keyboard-controller": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.1.tgz", - "integrity": "sha512-HUrZTaaDPxm94EVXlguwJB2gm6mc+VRTTzR66luFGZJZnL2SJoxN+dwsNW3twkwUVDrCPPA3U21q9YWUKVmwvg==", + "version": "1.14.4", + "resolved": "https://registry.npmjs.org/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.4.tgz", + "integrity": "sha512-hVt9KhK2dxBNtk4xHTnKLeO9Jv7v5h2TZlIeCQkbBLMd5NIJa4ll0GxIpbuutjP1ctPdhXUVpCfQzgXXJOYlzw==", + "license": "MIT", + "dependencies": { + "react-native-is-edge-to-edge": "^1.1.6" + }, "peerDependencies": { "react": "*", "react-native": "*", diff --git a/package.json b/package.json index 1b12bab2d1d8..d49ee9116aa0 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "react-native-image-picker": "^7.0.3", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9", "react-native-key-command": "^1.0.8", - "react-native-keyboard-controller": "1.14.1", + "react-native-keyboard-controller": "1.14.4", "react-native-launch-arguments": "^4.0.2", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", diff --git a/patches/react-native-keyboard-controller+1.14.1+001+disable-android.patch b/patches/react-native-keyboard-controller+1.14.4+001+disable-android.patch similarity index 96% rename from patches/react-native-keyboard-controller+1.14.1+001+disable-android.patch rename to patches/react-native-keyboard-controller+1.14.4+001+disable-android.patch index 6bb62155a98c..8d2d81aab40a 100644 --- a/patches/react-native-keyboard-controller+1.14.1+001+disable-android.patch +++ b/patches/react-native-keyboard-controller+1.14.4+001+disable-android.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt -index 7ef8b36..f4d44ff 100644 +index 93c20d3..df1e846 100644 --- a/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt +++ b/node_modules/react-native-keyboard-controller/android/src/main/java/com/reactnativekeyboardcontroller/views/EdgeToEdgeReactViewGroup.kt @@ -74,7 +74,7 @@ class EdgeToEdgeReactViewGroup( @@ -51,7 +51,7 @@ index 7ef8b36..f4d44ff 100644 } // endregion -@@ -219,7 +219,7 @@ class EdgeToEdgeReactViewGroup( +@@ -223,7 +223,7 @@ class EdgeToEdgeReactViewGroup( fun forceStatusBarTranslucent(isStatusBarTranslucent: Boolean) { if (active && this.isStatusBarTranslucent != isStatusBarTranslucent) { this.isStatusBarTranslucent = isStatusBarTranslucent diff --git a/src/CONFIG.ts b/src/CONFIG.ts index 8a30c8bf57c2..e5e9a9d1540a 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -103,4 +103,5 @@ export default { }, // to read more about StrictMode see: contributingGuides/STRICT_MODE.md USE_REACT_STRICT_MODE_IN_DEV: false, + ELECTRON_DISABLE_SECURITY_WARNINGS: 'true', } as const; diff --git a/src/components/LHNOptionsList/OptionRowRendererComponent/index.native.tsx b/src/components/LHNOptionsList/OptionRowRendererComponent/index.native.tsx index ff050f673951..2450ce800e0e 100644 --- a/src/components/LHNOptionsList/OptionRowRendererComponent/index.native.tsx +++ b/src/components/LHNOptionsList/OptionRowRendererComponent/index.native.tsx @@ -1,11 +1,14 @@ import {CellContainer} from '@shopify/flash-list'; import type {CellContainerProps} from '@shopify/flash-list/dist/native/cell-container/CellContainer'; +import type {ForwardedRef} from 'react'; +import {forwardRef} from 'react'; -function OptionRowRendererComponent(props: CellContainerProps) { +function OptionRowRendererComponent(props: CellContainerProps, ref: ForwardedRef) { return ( ); @@ -13,4 +16,4 @@ function OptionRowRendererComponent(props: CellContainerProps) { OptionRowRendererComponent.displayName = 'OptionRowRendererComponent'; -export default OptionRowRendererComponent; +export default forwardRef(OptionRowRendererComponent); From 2f00af7341289af78debe2f9a513f7fa9e065277 Mon Sep 17 00:00:00 2001 From: mkzie2 Date: Fri, 8 Nov 2024 14:26:03 +0700 Subject: [PATCH 094/296] fix: ValidateCodeActionModal is not dismissed on backdrop press --- src/components/ValidateCodeActionModal/index.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/ValidateCodeActionModal/index.tsx b/src/components/ValidateCodeActionModal/index.tsx index 461c780a50d0..0a2e74c5bccc 100644 --- a/src/components/ValidateCodeActionModal/index.tsx +++ b/src/components/ValidateCodeActionModal/index.tsx @@ -7,6 +7,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ValidateCodeActionModalProps} from './type'; @@ -54,6 +55,7 @@ function ValidateCodeActionModal({ isVisible={isVisible} onClose={hide} onModalHide={onModalHide ?? hide} + onBackdropPress={() => Navigation.dismissModal()} hideModalContentWhileAnimating useNativeDriver shouldUseModalPaddingStyle={false} From 471adcfde7157bf3f77dad8537a42c6030c6e98b Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 8 Nov 2024 10:26:13 +0100 Subject: [PATCH 095/296] feat: Step 4 UI --- src/CONST.ts | 24 ++ src/languages/en.ts | 39 +++ src/languages/es.ts | 39 +++ src/languages/params.ts | 5 + src/libs/ValidationUtils.ts | 26 ++ .../BeneficialOwnerCheck.tsx | 65 ++++ .../Address.tsx | 88 ++++++ .../Confirmation.tsx | 89 ++++++ .../DateOfBirth.tsx | 46 +++ .../Last4SSN.tsx | 67 ++++ .../Name.tsx | 52 +++ .../OwnershipPercentage.tsx | 73 +++++ .../BeneficialOwnerInfo.tsx | 295 +++++++++++++++++- .../BeneficialOwnersList.tsx | 115 +++++++ .../UploadOwnershipChart.tsx | 87 ++++++ .../ReimbursementAccount/NonUSD/WhyLink.tsx | 44 +++ .../utils/getValuesForBeneficialOwner.ts | 63 ++++ src/types/form/ReimbursementAccountForm.ts | 17 + src/types/onyx/ReimbursementAccount.ts | 13 + 19 files changed, 1232 insertions(+), 15 deletions(-) create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/DateOfBirth.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Last4SSN.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Name.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/OwnershipPercentage.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnersList.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/UploadOwnershipChart.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/WhyLink.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/utils/getValuesForBeneficialOwner.ts diff --git a/src/CONST.ts b/src/CONST.ts index 23a220e88ddb..0b618a67c72b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -612,6 +612,30 @@ const CONST = { AGREEMENTS: 'AgreementsStep', FINISH: 'FinishStep', }, + BENEFICIAL_OWNER_INFO_STEP: { + SUBSTEP: { + IS_USER_BENEFICIAL_OWNER: 1, + IS_ANYONE_ELSE_BENEFICIAL_OWNER: 2, + BENEFICIAL_OWNER_DETAILS_FORM: 3, + ARE_THERE_MORE_BENEFICIAL_OWNERS: 4, + OWNERSHIP_CHART: 5, + BENEFICIAL_OWNERS_LIST: 6, + }, + BENEFICIAL_OWNER_DATA: { + BENEFICIAL_OWNER_KEYS: 'beneficialOwnerKeys', + PREFIX: 'beneficialOwner', + FIRST_NAME: 'firstName', + LAST_NAME: 'lastName', + OWNERSHIP_PERCENTAGE: 'ownershipPercentage', + DOB: 'dob', + SSN_LAST_4: 'ssnLast4', + STREET: 'street', + CITY: 'city', + STATE: 'state', + ZIP_CODE: 'zipCode', + COUNTRY: 'country', + }, + }, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP_HEADER_HEIGHT: 40, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index b45243b65af5..5111b691254e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -41,6 +41,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1944,6 +1945,7 @@ const translations = { lastName: 'Please enter a valid last name.', noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit account or debit card.', validationAmounts: 'The validation amounts you entered are incorrect. Please double check your bank statement and try again.', + ownershipPercentage: 'Please enter a valid percentage number that is greater than 25', }, }, addPersonalBankAccountPage: { @@ -2219,6 +2221,43 @@ const translations = { byAddingThisBankAccount: "By adding this bank account, you confirm that you've read, understand, and accept", owners: 'Owners', }, + ownershipInfoStep: { + ownerInfo: 'Owner info', + businessOwner: 'Business owner', + signerInfo: 'Signer info', + doYouOwn: ({companyName}: CompanyNameParams) => `Do you own 25% or more of ${companyName}`, + doesAnyoneOwn: ({companyName}: CompanyNameParams) => `Does any individuals own 25% or more of ${companyName}`, + regulationsRequire: 'Regulations require us to verify the identity of any individual who owns more than 25% of the business.', + legalFirstName: 'Legal first name', + legalLastName: 'Legal last name', + whatsTheOwnersName: "What's the owner's legal name?", + whatsYourName: "What's your legal name?", + whatPercentage: 'What percentage of the business belongs to the owner?', + whatsYoursPercentage: 'What percentage of the business do you own?', + ownership: 'Ownership', + whatsTheOwnersDOB: "What's the owner's date of birth?", + whatsYourDOB: "What's your date of birth?", + whatsTheOwnersAddress: "What's the owner's address?", + whatsYourAddress: "What's your address?", + whatAreTheLast: "What are the last 4 digits of the owner's Social Security Number?", + whatsYourLast: 'What are the last 4 digits of your Social Security Number?', + dontWorry: "Don't worry, we don't do any personal credit checks!", + last4: 'Last 4 of SSN', + whyDoWeAsk: 'Why do we ask for this?', + letsDoubleCheck: 'Let’s double check that everything looks right.', + legalName: 'Legal name', + ownershipPercentage: 'Ownership percentage', + areThereOther: ({companyName}: CompanyNameParams) => `Are there other individuals who own 25% or more of ${companyName}`, + owners: 'Owners', + addCertified: 'Add a certified org chart that shows the beneficial owners', + regulationRequiresChart: 'Regulation requires us to collect a certified copy of the ownership chart that shows every individual or entity who owns 25% or more of the business.', + uploadEntity: 'Upload entity ownership chart', + noteEntity: 'Note: Entity ownership chart must be signed by your accountant, legal counsel, or notarized.', + certified: 'Certified entity ownership chart', + selectCountry: 'Select country', + findCountry: 'Find country', + address: 'Address', + }, validationStep: { headerTitle: 'Validate bank account', buttonText: 'Finish setup', diff --git a/src/languages/es.ts b/src/languages/es.ts index 31e713582168..449c7a04d719 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -39,6 +39,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1964,6 +1965,7 @@ const translations = { lastName: 'Por favor, introduce los apellidos.', noDefaultDepositAccountOrDebitCardAvailable: 'Por favor, añade una cuenta bancaria para depósitos o una tarjeta de débito.', validationAmounts: 'Los importes de validación que introduciste son incorrectos. Por favor, comprueba tu cuenta bancaria e inténtalo de nuevo.', + ownershipPercentage: 'Por favor, ingrese un número de porcentaje válido que sea mayor a 25', }, }, addPersonalBankAccountPage: { @@ -2242,6 +2244,43 @@ const translations = { byAddingThisBankAccount: 'Al añadir esta cuenta bancaria, confirmas que has leído, comprendido y aceptado', owners: 'Dueños', }, + ownershipInfoStep: { + ownerInfo: 'Información del propietario', + businessOwner: 'Propietario del negocio', + signerInfo: 'Información del firmante', + doYouOwn: ({companyName}: CompanyNameParams) => `¿Posee el 25% o más de ${companyName}?`, + doesAnyoneOwn: ({companyName}: CompanyNameParams) => `¿Alguien posee el 25% o más de ${companyName}?`, + regulationsRequire: 'Las regulaciones requieren que verifiquemos la identidad de cualquier persona que posea más del 25% del negocio.', + legalFirstName: 'Nombre legal', + legalLastName: 'Apellido legal', + whatsTheOwnersName: '¿Cuál es el nombre legal del propietario?', + whatsYourName: '¿Cuál es su nombre legal?', + whatPercentage: '¿Qué porcentaje del negocio pertenece al propietario?', + whatsYoursPercentage: '¿Qué porcentaje del negocio posee?', + ownership: 'Propiedad', + whatsTheOwnersDOB: '¿Cuál es la fecha de nacimiento del propietario?', + whatsYourDOB: '¿Cuál es su fecha de nacimiento?', + whatsTheOwnersAddress: '¿Cuál es la dirección del propietario?', + whatsYourAddress: '¿Cuál es su dirección?', + whatAreTheLast: '¿Cuáles son los últimos 4 dígitos del número de seguro social del propietario?', + whatsYourLast: '¿Cuáles son los últimos 4 dígitos de su número de seguro social?', + dontWorry: 'No se preocupe, ¡no realizamos ninguna verificación de crédito personal!', + last4: 'Últimos 4 del SSN', + whyDoWeAsk: '¿Por qué solicitamos esto?', + letsDoubleCheck: 'Verifiquemos que todo esté correcto.', + legalName: 'Nombre legal', + ownershipPercentage: 'Porcentaje de propiedad', + areThereOther: ({companyName}: CompanyNameParams) => `¿Hay otras personas que posean el 25% o más de ${companyName}?`, + owners: 'Propietarios', + addCertified: 'Agregue un organigrama certificado que muestre los propietarios beneficiarios', + regulationRequiresChart: 'La regulación nos exige recopilar una copia certificada del organigrama que muestre a cada persona o entidad que posea el 25% o más del negocio.', + uploadEntity: 'Subir organigrama de propiedad de la entidad', + noteEntity: 'Nota: El organigrama de propiedad de la entidad debe estar firmado por su contador, asesor legal o notariado.', + certified: 'Organigrama certificado de propiedad de la entidad', + selectCountry: 'Seleccionar país', + findCountry: 'Buscar país', + address: 'Dirección', + }, validationStep: { headerTitle: 'Validar cuenta bancaria', buttonText: 'Finalizar configuración', diff --git a/src/languages/params.ts b/src/languages/params.ts index 2d60c13c4dd0..c40bc01447c3 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -551,6 +551,10 @@ type CurrencyCodeParams = { currencyCode: string; }; +type CompanyNameParams = { + companyName: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -751,4 +755,5 @@ export type { AssignCardParams, ImportedTypesParams, CurrencyCodeParams, + CompanyNameParams, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 47e44bc049d2..e5ec86d03ba7 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -530,6 +530,31 @@ function isValidZipCodeInternational(zipCode: string): boolean { return /^[a-z0-9][a-z0-9\- ]{0,10}[a-z0-9]$/.test(zipCode); } +/** + * Validates the given value if it is correct ownership percentage + * @param value + * @param totalOwnedPercentage + * @param ownerBeingModifiedID + */ +function isValidOwnershipPercentage(value: string, totalOwnedPercentage: Record, ownerBeingModifiedID: string): boolean { + const parsedValue = Number(value); + const isValidNumber = !Number.isNaN(parsedValue) && parsedValue >= 25 && parsedValue <= 100; + + let totalOwnedPercentageSum = 0; + const totalOwnedPercentageKeys = Object.keys(totalOwnedPercentage); + totalOwnedPercentageKeys.forEach((key) => { + if (key === ownerBeingModifiedID) { + return; + } + + totalOwnedPercentageSum += totalOwnedPercentage[key]; + }); + + const isTotalSumValid = totalOwnedPercentageSum + parsedValue <= 100; + + return isValidNumber && isTotalSumValid; +} + export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, @@ -577,4 +602,5 @@ export { isValidEmail, isValidPhoneInternational, isValidZipCodeInternational, + isValidOwnershipPercentage, }; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx new file mode 100644 index 000000000000..4d108de6dae1 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx @@ -0,0 +1,65 @@ +import React, {useMemo, useState} from 'react'; +import FormProvider from '@components/Form/FormProvider'; +import type {Choice} from '@components/RadioButtons'; +import RadioButtons from '@components/RadioButtons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type BeneficialOwnerCheckProps = { + /** The title of the question */ + title: string; + + /** The default value of the radio button */ + defaultValue: boolean; + + /** Callback when the value is selected */ + onSelectedValue: (value: boolean) => void; +}; + +function BeneficialOwnerCheck({title, onSelectedValue, defaultValue}: BeneficialOwnerCheckProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [value, setValue] = useState(defaultValue); + + const handleSubmit = () => { + onSelectedValue(value); + }; + const handleSelectValue = (newValue: string) => setValue(newValue === 'true'); + const options = useMemo( + () => [ + { + label: translate('common.yes'), + value: 'true', + }, + { + label: translate('common.no'), + value: 'false', + }, + ], + [translate], + ); + + return ( + + {title} + {translate('ownershipInfoStep.regulationsRequire')} + + + ); +} + +BeneficialOwnerCheck.displayName = 'BeneficialOwnerCheck'; + +export default BeneficialOwnerCheck; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx new file mode 100644 index 000000000000..1629b90a5308 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx @@ -0,0 +1,88 @@ +import React, {useMemo, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import AddressStep from '@components/SubStepForms/AddressStep'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type NameProps = SubStepProps & {isUserEnteringHisOwnData: boolean; ownerBeingModifiedID: string}; + +const {STREET, CITY, STATE, ZIP_CODE, COUNTRY, PREFIX} = CONST.NON_USD_BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA; + +function Address({onNext, isEditing, onMove, isUserEnteringHisOwnData, ownerBeingModifiedID}: NameProps) { + const {translate} = useLocalize(); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const countryInputKey: `beneficialOwner_${string}_${string}` = `${PREFIX}_${ownerBeingModifiedID}_${COUNTRY}`; + const inputKeys = { + street: `${PREFIX}_${ownerBeingModifiedID}_${STREET}`, + city: `${PREFIX}_${ownerBeingModifiedID}_${CITY}`, + state: `${PREFIX}_${ownerBeingModifiedID}_${STATE}`, + zipCode: `${PREFIX}_${ownerBeingModifiedID}_${ZIP_CODE}`, + country: countryInputKey, + } as const; + + const defaultValues = { + street: reimbursementAccountDraft?.[inputKeys.street] ?? '', + city: reimbursementAccountDraft?.[inputKeys.city] ?? '', + state: reimbursementAccountDraft?.[inputKeys.state] ?? '', + zipCode: reimbursementAccountDraft?.[inputKeys.zipCode] ?? '', + country: (reimbursementAccountDraft?.[inputKeys.country] ?? '') as Country | '', + }; + + const formTitle = translate(isUserEnteringHisOwnData ? 'ownershipInfoStep.whatsYourAddress' : 'ownershipInfoStep.whatsTheOwnersAddress'); + + // Has to be stored in state and updated on country change due to the fact that we can't relay on onyxValues when user is editing the form (draft values are not being saved in that case) + const [shouldDisplayStateSelector, setShouldDisplayStateSelector] = useState( + defaultValues.country === CONST.COUNTRY.US || defaultValues.country === CONST.COUNTRY.CA || defaultValues.country === '', + ); + + const stepFieldsWithState = useMemo( + () => [inputKeys.street, inputKeys.city, inputKeys.state, inputKeys.zipCode, countryInputKey], + [countryInputKey, inputKeys.city, inputKeys.state, inputKeys.street, inputKeys.zipCode], + ); + const stepFieldsWithoutState = useMemo( + () => [inputKeys.street, inputKeys.city, inputKeys.zipCode, countryInputKey], + [countryInputKey, inputKeys.city, inputKeys.street, inputKeys.zipCode], + ); + + const stepFields = shouldDisplayStateSelector ? stepFieldsWithState : stepFieldsWithoutState; + + const handleCountryChange = (country: unknown) => { + if (typeof country !== 'string' || country === '') { + return; + } + setShouldDisplayStateSelector(country === CONST.COUNTRY.US || country === CONST.COUNTRY.CA); + }; + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: stepFields, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + + isEditing={isEditing} + onNext={onNext} + onMove={onMove} + formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + formTitle={formTitle} + formPOBoxDisclaimer={translate('common.noPO')} + onSubmit={handleSubmit} + stepFields={stepFields} + inputFieldsIDs={inputKeys} + defaultValues={defaultValues} + onCountryChange={handleCountryChange} + shouldDisplayStateSelector={shouldDisplayStateSelector} + shouldDisplayCountrySelector + /> + ); +} + +Address.displayName = 'Address'; + +export default Address; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx new file mode 100644 index 000000000000..6b880e8b3ad1 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import SafeAreaConsumer from '@components/SafeAreaConsumer'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getValuesForBeneficialOwner from '@pages/ReimbursementAccount/NonUSD/utils/getValuesForBeneficialOwner'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type ConfirmationProps = SubStepProps & {ownerBeingModifiedID: string}; + +function Confirmation({onNext, onMove, ownerBeingModifiedID}: ConfirmationProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const values = getValuesForBeneficialOwner(ownerBeingModifiedID, reimbursementAccountDraft); + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + {translate('ownershipInfoStep.letsDoubleCheck')} + { + onMove(0); + }} + /> + { + onMove(1); + }} + /> + { + onMove(2); + }} + /> + { + onMove(4); + }} + /> + { + onMove(3); + }} + /> + +