From 08337c3bc03cea5a74ee0aa22818826fb3f4b964 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 20 Sep 2023 15:31:39 +0700 Subject: [PATCH 0001/1081] leave room when has no comment --- src/libs/actions/Report.js | 11 +++++++---- src/pages/home/ReportScreen.js | 15 ++++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 2a34c839a94e..759f245ac0a5 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1777,8 +1777,9 @@ function getCurrentUserAccountID() { * Leave a report by setting the state to submitted and closed * * @param {String} reportID + * @param {Boolean} shouldNavigate should navigate after leaving room or not */ -function leaveRoom(reportID) { +function leaveRoom(reportID, shouldNavigate = true) { const report = lodashGet(allReports, [reportID], {}); const reportKeys = _.keys(report); API.write( @@ -1819,10 +1820,12 @@ function leaveRoom(reportID) { }, ); Navigation.dismissModal(); - if (Navigation.getTopmostReportId() === reportID) { - Navigation.goBack(); + if (shouldNavigate) { + if (Navigation.getTopmostReportId() === reportID) { + Navigation.goBack(); + } + navigateToConciergeChat(); } - navigateToConciergeChat(); } /** diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 8528b8f213a9..13e77923ad5c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,6 +1,6 @@ import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; -import {useFocusEffect} from '@react-navigation/native'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; @@ -151,6 +151,7 @@ function ReportScreen({ const flatListRef = useRef(); const reactionListRef = useRef(); const prevReport = usePrevious(report); + const isFocused = useIsFocused(); const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0); const [isBannerVisible, setIsBannerVisible] = useState(true); @@ -312,6 +313,18 @@ function ReportScreen({ [report, isLoading, shouldHideReport, isDefaultReport, isOptimisticDelete], ); + useEffect(() => { + if (isFocused) { + return; + } + + if (ReportUtils.isThread(report) && report.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN) { + Report.leaveRoom(report.reportID, false); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFocused]); + return ( Date: Wed, 27 Sep 2023 10:53:59 +0200 Subject: [PATCH 0002/1081] ref: started migrating Task lib to TS --- src/libs/actions/{Task.js => Task.ts} | 65 ++++++++++++--------------- src/types/onyx/Report.ts | 2 + 2 files changed, 31 insertions(+), 36 deletions(-) rename src/libs/actions/{Task.js => Task.ts} (93%) diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.ts similarity index 93% rename from src/libs/actions/Task.js rename to src/libs/actions/Task.ts index 963bfebb7eb2..b5c813e14521 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.ts @@ -14,21 +14,22 @@ import * as ReportActionsUtils from '../ReportActionsUtils'; import * as Expensicons from '../../components/Icon/Expensicons'; import * as LocalePhoneNumber from '../LocalePhoneNumber'; import * as Localize from '../Localize'; +import {PersonalDetails, Report, Task} from '../../types/onyx'; -let currentUserEmail; -let currentUserAccountID; +let currentUserEmail: string; +let currentUserAccountID: number; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - currentUserEmail = lodashGet(val, 'email', ''); - currentUserAccountID = lodashGet(val, 'accountID', 0); + callback: (value) => { + currentUserEmail = value?.email ?? ''; + currentUserAccountID = value?.accountID ?? 0; }, }); -let allPersonalDetails; +let allPersonalDetails: Record | null; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = val), + callback: (value) => (allPersonalDetails = value), }); /** @@ -52,16 +53,16 @@ function clearOutTaskInfo() { * 3. The chat report between you and the assignee * 3a. The CreatedReportAction for the assignee chat report * 3b. The TaskReportAction on the assignee chat report - * - * @param {String} parentReportID - * @param {String} title - * @param {String} description - * @param {String} assigneeEmail - * @param {Number} assigneeAccountID - * @param {Object} assigneeChatReport - The chat report between you and the assignee - * @param {String} policyID - the policyID of the parent report */ -function createTaskAndNavigate(parentReportID, title, description, assigneeEmail, assigneeAccountID = 0, assigneeChatReport = null, policyID = CONST.POLICY.OWNER_EMAIL_FAKE) { +function createTaskAndNavigate( + parentReportID: string, + title: string, + description: string, + assigneeEmail: string, + assigneeAccountID = 0, + assigneeChatReport: Report | null = null, + policyID = CONST.POLICY.OWNER_EMAIL_FAKE, +) { const optimisticTaskReport = ReportUtils.buildOptimisticTaskReport(currentUserAccountID, assigneeAccountID, parentReportID, title, description, policyID); const assigneeChatReportID = assigneeChatReport ? assigneeChatReport.reportID : 0; @@ -203,12 +204,8 @@ function createTaskAndNavigate(parentReportID, title, description, assigneeEmail assignee: assigneeEmail, assigneeAccountID, assigneeChatReportID, - assigneeChatReportActionID: - assigneeChatReportOnyxData && assigneeChatReportOnyxData.optimisticAssigneeAddComment - ? assigneeChatReportOnyxData.optimisticAssigneeAddComment.reportAction.reportActionID - : 0, - assigneeChatCreatedReportActionID: - assigneeChatReportOnyxData && assigneeChatReportOnyxData.optimisticChatCreatedReportAction ? assigneeChatReportOnyxData.optimisticChatCreatedReportAction.reportActionID : 0, + assigneeChatReportActionID: assigneeChatReportOnyxData?.optimisticAssigneeAddComment.reportAction.reportActionID ?? 0, + assigneeChatCreatedReportActionID: assigneeChatReportOnyxData?.optimisticChatCreatedReportAction.reportActionID ?? 0, }, {optimisticData, successData, failureData}, ); @@ -305,7 +302,7 @@ function completeTask(taskReport, taskTitle) { * @param {Object} taskReport task report * @param {String} taskTitle Title of the task */ -function reopenTask(taskReport, taskTitle) { +function reopenTask(taskReport: Report, taskTitle: string) { const taskReportID = taskReport.reportID; const message = `reopened task: ${taskTitle}`; const reopenedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKREOPENED, message); @@ -393,15 +390,15 @@ function reopenTask(taskReport, taskTitle) { * @param {Object} editedTask * @param {Object} assigneeChatReport - The chat report between you and the assignee */ -function editTaskAndNavigate(report, ownerAccountID, {title, description, assignee = '', assigneeAccountID = 0}, assigneeChatReport = null) { +function editTaskAndNavigate(report: Report, ownerAccountID: number, {title, description, assignee = '', assigneeAccountID = 0}: Task, assigneeChatReport: Report | null = null) { // Create the EditedReportAction on the task const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail); // Sometimes title or description is undefined, so we need to check for that, and we provide it to multiple functions - const reportName = (title || report.reportName).trim(); + const reportName = (title ?? report?.reportName).trim(); // Description can be unset, so we default to an empty string if so - const reportDescription = (!_.isUndefined(description) ? description : lodashGet(report, 'description', '')).trim(); + const reportDescription = (description ?? report.description ?? '').trim(); let assigneeChatReportOnyxData; const assigneeChatReportID = assigneeChatReport ? assigneeChatReport.reportID : 0; @@ -418,8 +415,8 @@ function editTaskAndNavigate(report, ownerAccountID, {title, description, assign value: { reportName, description: reportDescription, - managerID: assigneeAccountID || report.managerID, - managerEmail: assignee || report.managerEmail, + managerID: assigneeAccountID ?? report.managerID, + managerEmail: assignee ?? report.managerEmail, pendingFields: { ...(title && {reportName: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(description && {description: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), @@ -487,12 +484,8 @@ function editTaskAndNavigate(report, ownerAccountID, {title, description, assign assigneeAccountID: assigneeAccountID || report.managerID, editedTaskReportActionID: editTaskReportAction.reportActionID, assigneeChatReportID, - assigneeChatReportActionID: - assigneeChatReportOnyxData && assigneeChatReportOnyxData.optimisticAssigneeAddComment - ? assigneeChatReportOnyxData.optimisticAssigneeAddComment.reportAction.reportActionID - : 0, - assigneeChatCreatedReportActionID: - assigneeChatReportOnyxData && assigneeChatReportOnyxData.optimisticChatCreatedReportAction ? assigneeChatReportOnyxData.optimisticChatCreatedReportAction.reportActionID : 0, + assigneeChatReportActionID: assigneeChatReportOnyxData.optimisticAssigneeAddComment.reportAction.reportActionID ?? 0, + assigneeChatCreatedReportActionID: assigneeChatReportOnyxData.optimisticChatCreatedReportAction.reportActionID ?? 0, }, {optimisticData, successData, failureData}, ); @@ -500,10 +493,10 @@ function editTaskAndNavigate(report, ownerAccountID, {title, description, assign Navigation.dismissModal(report.reportID); } -function editTaskAssigneeAndNavigate(report, ownerAccountID, assigneeEmail, assigneeAccountID = 0, assigneeChatReport = null) { +function editTaskAssigneeAndNavigate(report: Report, ownerAccountID: number, assigneeEmail: string, assigneeAccountID = 0, assigneeChatReport: Report | null = null) { // Create the EditedReportAction on the task const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail); - const reportName = report.reportName.trim(); + const reportName = report.reportName?.trim(); let assigneeChatReportOnyxData; const assigneeChatReportID = assigneeChatReport ? assigneeChatReport.reportID : 0; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 88caa683305d..2e944c156a64 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -83,6 +83,8 @@ type Report = { participantAccountIDs?: number[]; total?: number; currency?: string; + description?: string; + managerEmail?: string; }; export default Report; From 87998bf588a4fa826f06767b898f022111a5806d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 27 Sep 2023 13:20:31 +0200 Subject: [PATCH 0003/1081] ref: move all methods to TS --- src/libs/actions/Task.ts | 155 +++++++++++++-------------------------- src/types/onyx/Report.ts | 1 + 2 files changed, 51 insertions(+), 105 deletions(-) diff --git a/src/libs/actions/Task.ts b/src/libs/actions/Task.ts index b5c813e14521..adc7c41f4cfe 100644 --- a/src/libs/actions/Task.ts +++ b/src/libs/actions/Task.ts @@ -1,6 +1,4 @@ import Onyx from 'react-native-onyx'; -import lodashGet from 'lodash/get'; -import _ from 'underscore'; import ONYXKEYS from '../../ONYXKEYS'; import * as API from '../API'; import * as ReportUtils from '../ReportUtils'; @@ -215,10 +213,8 @@ function createTaskAndNavigate( /** * Complete a task - * @param {Object} taskReport task report - * @param {String} taskTitle Title of the task */ -function completeTask(taskReport, taskTitle) { +function completeTask(taskReport: Report, taskTitle: string) { const taskReportID = taskReport.reportID; const message = `completed task: ${taskTitle}`; const completedTaskReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED, message); @@ -251,6 +247,7 @@ function completeTask(taskReport, taskTitle) { }, }, ]; + const failureData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -274,7 +271,7 @@ function completeTask(taskReport, taskTitle) { // Multiple report actions can link to the same child. Both share destination (task parent) and assignee report link to the same report action. // We need to find and update the other parent report action (in assignee report). More info https://github.com/Expensify/App/issues/23920#issuecomment-1663092717 const assigneeReportAction = ReportUtils.getTaskParentReportActionIDInAssigneeReport(taskReport); - if (!_.isEmpty(assigneeReportAction)) { + if (Object.keys(assigneeReportAction).length > 0) { const optimisticDataForClonedParentReportAction = ReportUtils.getOptimisticDataForParentReportAction( taskReportID, completedTaskReportAction.created, @@ -282,7 +279,7 @@ function completeTask(taskReport, taskTitle) { assigneeReportAction.reportID, assigneeReportAction.reportActionID, ); - if (!_.isEmpty(optimisticDataForClonedParentReportAction)) { + if (Object.keys(optimisticDataForClonedParentReportAction).length > 0) { optimisticData.push(optimisticDataForClonedParentReportAction); } } @@ -299,8 +296,6 @@ function completeTask(taskReport, taskTitle) { /** * Reopen a closed task - * @param {Object} taskReport task report - * @param {String} taskTitle Title of the task */ function reopenTask(taskReport: Report, taskTitle: string) { const taskReportID = taskReport.reportID; @@ -361,7 +356,7 @@ function reopenTask(taskReport: Report, taskTitle: string) { // Multiple report actions can link to the same child. Both share destination (task parent) and assignee report link to the same report action. // We need to find and update the other parent report action (in assignee report). More info https://github.com/Expensify/App/issues/23920#issuecomment-1663092717 const assigneeReportAction = ReportUtils.getTaskParentReportActionIDInAssigneeReport(taskReport); - if (!_.isEmpty(assigneeReportAction)) { + if (Object.keys(assigneeReportAction).length > 0 && taskReportID) { const optimisticDataForClonedParentReportAction = ReportUtils.getOptimisticDataForParentReportAction( taskReportID, reopenedTaskReportAction.created, @@ -369,7 +364,7 @@ function reopenTask(taskReport: Report, taskTitle: string) { assigneeReportAction.reportID, assigneeReportAction.reportActionID, ); - if (!_.isEmpty(optimisticDataForClonedParentReportAction)) { + if (Object.keys(optimisticDataForClonedParentReportAction).length > 0) { optimisticData.push(optimisticDataForClonedParentReportAction); } } @@ -384,12 +379,6 @@ function reopenTask(taskReport: Report, taskTitle: string) { ); } -/** - * @param {object} report - * @param {Number} ownerAccountID - * @param {Object} editedTask - * @param {Object} assigneeChatReport - The chat report between you and the assignee - */ function editTaskAndNavigate(report: Report, ownerAccountID: number, {title, description, assignee = '', assigneeAccountID = 0}: Task, assigneeChatReport: Report | null = null) { // Create the EditedReportAction on the task const editTaskReportAction = ReportUtils.buildOptimisticEditedTaskReportAction(currentUserEmail); @@ -458,7 +447,7 @@ function editTaskAndNavigate(report: Report, ownerAccountID: number, {title, des // If we make a change to the assignee, we want to add a comment to the assignee's chat // Check if the assignee actually changed - if (assigneeAccountID && assigneeAccountID !== report.managerID && assigneeAccountID !== ownerAccountID && assigneeChatReport) { + if (assigneeAccountID && assigneeAccountID !== report.managerID && assigneeAccountID !== ownerAccountID && assigneeChatReport && report.reportID) { assigneeChatReportOnyxData = ReportUtils.getTaskAssigneeChatOnyxData( currentUserAccountID, assignee, @@ -480,12 +469,12 @@ function editTaskAndNavigate(report: Report, ownerAccountID: number, {title, des taskReportID: report.reportID, title: reportName, description: reportDescription, - assignee: assignee || report.managerEmail, - assigneeAccountID: assigneeAccountID || report.managerID, + assignee: assignee ?? report.managerEmail, + assigneeAccountID: assigneeAccountID ?? report.managerID, editedTaskReportActionID: editTaskReportAction.reportActionID, assigneeChatReportID, - assigneeChatReportActionID: assigneeChatReportOnyxData.optimisticAssigneeAddComment.reportAction.reportActionID ?? 0, - assigneeChatCreatedReportActionID: assigneeChatReportOnyxData.optimisticChatCreatedReportAction.reportActionID ?? 0, + assigneeChatReportActionID: assigneeChatReportOnyxData?.optimisticAssigneeAddComment.reportAction.reportActionID ?? 0, + assigneeChatCreatedReportActionID: assigneeChatReportOnyxData?.optimisticChatCreatedReportAction.reportActionID ?? 0, }, {optimisticData, successData, failureData}, ); @@ -512,8 +501,8 @@ function editTaskAssigneeAndNavigate(report: Report, ownerAccountID: number, ass key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, value: { reportName, - managerID: assigneeAccountID || report.managerID, - managerEmail: assigneeEmail || report.managerEmail, + managerID: assigneeAccountID ?? report.managerID, + managerEmail: assigneeEmail ?? report.managerEmail, }, }, ]; @@ -533,7 +522,7 @@ function editTaskAssigneeAndNavigate(report: Report, ownerAccountID: number, ass // If we make a change to the assignee, we want to add a comment to the assignee's chat // Check if the assignee actually changed - if (assigneeAccountID && assigneeAccountID !== report.managerID && assigneeAccountID !== ownerAccountID && assigneeChatReport) { + if (assigneeAccountID && assigneeAccountID !== report.managerID && assigneeAccountID !== ownerAccountID && assigneeChatReport && report.reportID) { assigneeChatReportOnyxData = ReportUtils.getTaskAssigneeChatOnyxData( currentUserAccountID, assigneeEmail, @@ -553,16 +542,12 @@ function editTaskAssigneeAndNavigate(report: Report, ownerAccountID: number, ass 'EditTaskAssignee', { taskReportID: report.reportID, - assignee: assigneeEmail || report.managerEmail, - assigneeAccountID: assigneeAccountID || report.managerID, + assignee: assigneeEmail ?? report.managerEmail, + assigneeAccountID: assigneeAccountID ?? report.managerID, editedTaskReportActionID: editTaskReportAction.reportActionID, assigneeChatReportID, - assigneeChatReportActionID: - assigneeChatReportOnyxData && assigneeChatReportOnyxData.optimisticAssigneeAddComment - ? assigneeChatReportOnyxData.optimisticAssigneeAddComment.reportAction.reportActionID - : 0, - assigneeChatCreatedReportActionID: - assigneeChatReportOnyxData && assigneeChatReportOnyxData.optimisticChatCreatedReportAction ? assigneeChatReportOnyxData.optimisticChatCreatedReportAction.reportActionID : 0, + assigneeChatReportActionID: assigneeChatReportOnyxData?.optimisticAssigneeAddComment.reportAction.reportActionID ?? 0, + assigneeChatCreatedReportActionID: assigneeChatReportOnyxData?.optimisticChatCreatedReportAction.reportActionID ?? 0, }, {optimisticData, successData, failureData}, ); @@ -575,49 +560,43 @@ function editTaskAssigneeAndNavigate(report: Report, ownerAccountID: number, ass * * @param {Object} report */ -function setTaskReport(report) { +function setTaskReport(report: Report) { Onyx.merge(ONYXKEYS.TASK, {report}); } /** * Sets the title and description values for the task - * @param {string} title - * @param {string} description */ -function setDetailsValue(title, description) { +function setDetailsValue(title: string, description: string) { // This is only needed for creation of a new task and so it should only be stored locally Onyx.merge(ONYXKEYS.TASK, {title: title.trim(), description: description.trim()}); } /** * Sets the title value for the task - * @param {string} title */ -function setTitleValue(title) { +function setTitleValue(title: string) { Onyx.merge(ONYXKEYS.TASK, {title: title.trim()}); } /** * Sets the description value for the task - * @param {string} description */ -function setDescriptionValue(description) { +function setDescriptionValue(description: string) { Onyx.merge(ONYXKEYS.TASK, {description: description.trim()}); } /** * Sets the shareDestination value for the task - * @param {string} shareDestination */ -function setShareDestinationValue(shareDestination) { +function setShareDestinationValue(shareDestination: string) { // This is only needed for creation of a new task and so it should only be stored locally Onyx.merge(ONYXKEYS.TASK, {shareDestination}); } /* Sets the assigneeChatReport details for the task - * @param {Object} chatReport */ -function setAssigneeChatReport(chatReport) { +function setAssigneeChatReport(chatReport: Report) { Onyx.merge(ONYXKEYS.TASK, {assigneeChatReport: chatReport}); } @@ -625,14 +604,9 @@ function setAssigneeChatReport(chatReport) { * Sets the assignee value for the task and checks for an existing chat with the assignee * If there is no existing chat, it creates an optimistic chat report * It also sets the shareDestination as that chat report if a share destination isn't already set - * @param {string} assigneeEmail - * @param {Number} assigneeAccountID - * @param {string} shareDestination - * @param {boolean} isCurrentUser */ - -function setAssigneeValue(assigneeEmail, assigneeAccountID, shareDestination, isCurrentUser = false) { - let chatReport; +function setAssigneeValue(assigneeEmail: string, assigneeAccountID: number, shareDestination: string, isCurrentUser = false) { + let chatReport: Report | undefined; if (!isCurrentUser) { chatReport = ReportUtils.getChatByParticipants([assigneeAccountID]); @@ -644,14 +618,16 @@ function setAssigneeValue(assigneeEmail, assigneeAccountID, shareDestination, is // However, the DM doesn't exist yet - and will be created optimistically once the task is created // We don't want to show the new DM yet, because if you select an assignee and then change the assignee, the previous DM will still be shown // So here, we create it optimistically to share it with the assignee, but we have to hide it until the task is created - chatReport.isHidden = true; + if (chatReport) { + chatReport.isHidden = true; + } Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport); // If this is an optimistic report, we likely don't have their personal details yet so we set it here optimistically as well const optimisticPersonalDetailsListAction = { accountID: assigneeAccountID, - avatar: lodashGet(allPersonalDetails, [assigneeAccountID, 'avatar'], UserUtils.getDefaultAvatarURL(assigneeAccountID)), - displayName: lodashGet(allPersonalDetails, [assigneeAccountID, 'displayName'], assigneeEmail), + avatar: allPersonalDetails?.[assigneeAccountID]?.avatar ?? UserUtils.getDefaultAvatarURL(assigneeAccountID), + displayName: allPersonalDetails?.[assigneeAccountID]?.displayName ?? assigneeEmail, login: assigneeEmail, }; Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[assigneeAccountID]: optimisticPersonalDetailsListAction}); @@ -662,7 +638,7 @@ function setAssigneeValue(assigneeEmail, assigneeAccountID, shareDestination, is // If there is no share destination set, automatically set it to the assignee chat report // This allows for a much quicker process when creating a new task and is likely the desired share destination most times if (!shareDestination) { - setShareDestinationValue(chatReport.reportID); + setShareDestinationValue(chatReport?.reportID ?? ''); } } @@ -677,31 +653,25 @@ function setAssigneeValue(assigneeEmail, assigneeAccountID, shareDestination, is /** * Sets the parentReportID value for the task - * @param {string} parentReportID */ -function setParentReportID(parentReportID) { +function setParentReportID(parentReportID: string) { // This is only needed for creation of a new task and so it should only be stored locally Onyx.merge(ONYXKEYS.TASK, {parentReportID}); } /** * Clears out the task info from the store and navigates to the NewTaskDetails page - * @param {string} reportID */ -function clearOutTaskInfoAndNavigate(reportID) { +function clearOutTaskInfoAndNavigate(reportID: string) { clearOutTaskInfo(); setParentReportID(reportID); - Navigation.navigate(ROUTES.NEW_TASK_DETAILS); + Navigation.navigate(ROUTES.NEW_TASK_DETAILS, ''); } /** * Get the assignee data - * - * @param {Number} assigneeAccountID - * @param {Object} personalDetails - * @returns {Object} */ -function getAssignee(assigneeAccountID, personalDetails) { +function getAssignee(assigneeAccountID: number, personalDetails: Record) { const details = personalDetails[assigneeAccountID]; if (!details) { return { @@ -719,18 +689,15 @@ function getAssignee(assigneeAccountID, personalDetails) { /** * Get the share destination data - * @param {Object} reportID - * @param {Object} reports - * @param {Object} personalDetails - * @returns {Object} * */ -function getShareDestination(reportID, reports, personalDetails) { - const report = lodashGet(reports, `report_${reportID}`, {}); +function getShareDestination(reportID: string, reports: Record, personalDetails: Record) { + const report = reports[`report_${reportID}`] ?? {}; let subtitle = ''; if (ReportUtils.isChatReport(report) && ReportUtils.isDM(report) && ReportUtils.hasSingleParticipant(report)) { - const participantAccountID = lodashGet(report, 'participantAccountIDs[0]'); - const displayName = lodashGet(personalDetails, [participantAccountID, 'displayName']); - const login = lodashGet(personalDetails, [participantAccountID, 'login']); + const participantAccountID = report.participantAccountIDs?.[0]; + + const displayName = personalDetails[participantAccountID ?? 0]?.displayName ?? ''; + const login = personalDetails[participantAccountID ?? 0]?.login ?? ''; subtitle = LocalePhoneNumber.formatPhoneNumber(login || displayName); } else { subtitle = ReportUtils.getChatRoomSubtitle(report); @@ -744,12 +711,8 @@ function getShareDestination(reportID, reports, personalDetails) { /** * Cancels a task by setting the report state to SUBMITTED and status to CLOSED - * @param {string} taskReportID - * @param {string} taskTitle - * @param {number} originalStateNum - * @param {number} originalStatusNum */ -function cancelTask(taskReportID, taskTitle, originalStateNum, originalStatusNum) { +function cancelTask(taskReportID: string, taskTitle: string, originalStateNum: number, originalStatusNum: number) { const message = `deleted task: ${taskTitle}`; const optimisticCancelReportAction = ReportUtils.buildOptimisticTaskReportAction(taskReportID, CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED, message); const optimisticReportActionID = optimisticCancelReportAction.reportActionID; @@ -853,11 +816,8 @@ function dismissModalAndClearOutTaskInfo() { /** * Returns Task assignee accountID - * - * @param {Object} taskReport - * @returns {Number|null} */ -function getTaskAssigneeAccountID(taskReport) { +function getTaskAssigneeAccountID(taskReport: Report): number | null { if (!taskReport) { return null; } @@ -867,26 +827,20 @@ function getTaskAssigneeAccountID(taskReport) { } const reportAction = ReportActionsUtils.getParentReportAction(taskReport); - return lodashGet(reportAction, 'childManagerAccountID'); + return reportAction.childManagerAccountID; } /** * Returns Task owner accountID - * - * @param {Object} taskReport - * @returns {Number|null} */ -function getTaskOwnerAccountID(taskReport) { - return lodashGet(taskReport, 'ownerAccountID', null); +function getTaskOwnerAccountID(taskReport: Report): number | null { + return taskReport.ownerAccountID ?? null; } /** * Check if you're allowed to modify the task - anyone that has write access to the report can modify the task - * @param {Object} taskReport - * @param {Number} sessionAccountID - * @returns {Boolean} */ -function canModifyTask(taskReport, sessionAccountID) { +function canModifyTask(taskReport: Report, sessionAccountID: number): boolean { if (sessionAccountID === getTaskOwnerAccountID(taskReport) || sessionAccountID === getTaskAssigneeAccountID(taskReport)) { return true; } @@ -898,23 +852,14 @@ function canModifyTask(taskReport, sessionAccountID) { return ReportUtils.isAllowedToComment(parentReport); } -/** - * @param {String} reportID - */ -function clearEditTaskErrors(reportID) { +function clearEditTaskErrors(reportID: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { pendingFields: null, errorFields: null, }); } -/** - * @param {string} actionName - * @param {string} reportID - * @param {boolean} isCreateTaskAction - * @returns {string} - */ -function getTaskReportActionMessage(actionName, reportID, isCreateTaskAction) { +function getTaskReportActionMessage(actionName: string, reportID: string, isCreateTaskAction: boolean): string { const report = ReportUtils.getReport(reportID); if (isCreateTaskAction) { return `Created a task: ${report.reportName}`; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 2e944c156a64..ccb33f60531e 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -85,6 +85,7 @@ type Report = { currency?: string; description?: string; managerEmail?: string; + isHidden?: boolean; }; export default Report; From 1c2690a36e376d9b5dbae91e43a538013dd4764d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 2 Oct 2023 16:28:03 +0200 Subject: [PATCH 0004/1081] ref: move OptionsListUtils to TS --- ...ptionsListUtils.js => OptionsListUtils.ts} | 679 ++++++++---------- src/types/onyx/IOU.ts | 1 + src/types/onyx/Report.ts | 6 + src/types/onyx/ReportAction.ts | 1 + src/types/onyx/index.ts | 3 +- 5 files changed, 320 insertions(+), 370 deletions(-) rename src/libs/{OptionsListUtils.js => OptionsListUtils.ts} (69%) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.ts similarity index 69% rename from src/libs/OptionsListUtils.js rename to src/libs/OptionsListUtils.ts index e0f334ca36af..f76e7c84c3cb 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.ts @@ -1,8 +1,9 @@ /* eslint-disable no-continue */ +import {SvgProps} from 'react-native-svg'; import _ from 'underscore'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import lodashOrderBy from 'lodash/orderBy'; -import lodashGet from 'lodash/get'; +import {ValueOf} from 'type-fest'; import Str from 'expensify-common/lib/str'; import {parsePhoneNumber} from 'awesome-phonenumber'; import ONYXKEYS from '../ONYXKEYS'; @@ -18,42 +19,96 @@ import * as UserUtils from './UserUtils'; import * as ReportActionUtils from './ReportActionsUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as ErrorUtils from './ErrorUtils'; +import {Beta, Login, Participant, PersonalDetails, Policy, PolicyCategory, Report, ReportAction} from '../types/onyx'; +import * as OnyxCommon from '../types/onyx/OnyxCommon'; + +type PersonalDetailsCollection = Record; +type Avatar = { + source: string | (() => void); + name: string; + type: ValueOf; + id: number | string; +}; + +type Option = { + text?: string | null; + boldStyle?: boolean; + alternateText?: string | null; + alternateTextMaxLines?: number; + icons?: Avatar[] | null; + login?: string | null; + reportID?: string | null; + hasDraftComment?: boolean; + keyForList?: string | null; + searchText?: string | null; + isPinned?: boolean; + isChatRoom?: boolean; + hasOutstandingIOU?: boolean; + customIcon?: {src: React.FC; color: string}; + participantsList?: Array> | null; + descriptiveText?: string; + type?: string; + tooltipText?: string | null; + brickRoadIndicator?: ValueOf | null | ''; + phoneNumber?: string | null; + pendingAction?: Record | null; + allReportErrors?: OnyxCommon.Errors | null; + isDefaultRoom: boolean; + isArchivedRoom: boolean; + isPolicyExpenseChat: boolean; + isExpenseReport: boolean; + isMoneyRequestReport?: boolean; + isThread?: boolean; + isTaskReport?: boolean; + shouldShowSubscript: boolean; + ownerAccountID?: number | null; + isUnread?: boolean; + iouReportID?: string | number | null; + isWaitingOnBankAccount?: boolean; + policyID?: string | null; + subtitle?: string | null; + accountID: number | null; + iouReportAmount: number; + isIOUReportOwner: boolean | null; + isOptimisticAccount?: boolean; +}; + +type Tag = {enabled: boolean; name: string}; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can * be configured to display different results based on the options passed to the private getOptions() method. Public * methods should be named for the views they build options for and then exported for use in a component. */ - -let currentUserLogin; -let currentUserAccountID; +let currentUserLogin: string | undefined; +let currentUserAccountID: number | undefined; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { - currentUserLogin = val && val.email; - currentUserAccountID = val && val.accountID; + callback: (value) => { + currentUserLogin = value?.email; + currentUserAccountID = value?.accountID; }, }); -let loginList; +let loginList: OnyxEntry; Onyx.connect({ key: ONYXKEYS.LOGIN_LIST, - callback: (val) => (loginList = _.isEmpty(val) ? {} : val), + callback: (value) => (loginList = Object.keys(value ?? {}).length === 0 ? {} : value), }); -let allPersonalDetails; +let allPersonalDetails: OnyxEntry>; Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => (allPersonalDetails = _.isEmpty(val) ? {} : val), + callback: (value) => (allPersonalDetails = Object.keys(value ?? {}).length === 0 ? {} : value), }); -let preferredLocale; +let preferredLocale: OnyxEntry>; Onyx.connect({ key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: (val) => (preferredLocale = val || CONST.LOCALES.DEFAULT), + callback: (value) => (preferredLocale = value ?? CONST.LOCALES.DEFAULT), }); -const policies = {}; +const policies: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, callback: (policy, key) => { @@ -65,8 +120,8 @@ Onyx.connect({ }, }); -const lastReportActions = {}; -const allSortedReportActions = {}; +const lastReportActions: Record = {}; +const allSortedReportActions: Record = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, callback: (actions, key) => { @@ -76,11 +131,11 @@ Onyx.connect({ const sortedReportActions = ReportActionUtils.getSortedReportActions(_.toArray(actions), true); const reportID = CollectionUtils.extractCollectionItemID(key); allSortedReportActions[reportID] = sortedReportActions; - lastReportActions[reportID] = _.first(sortedReportActions); + lastReportActions[reportID] = sortedReportActions[0]; }, }); -const policyExpenseReports = {}; +const policyExpenseReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, callback: (report, key) => { @@ -93,16 +148,14 @@ Onyx.connect({ /** * Get the option for a policy expense report. - * @param {Object} report - * @returns {Object} */ -function getPolicyExpenseReportOption(report) { - const expenseReport = policyExpenseReports[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; +function getPolicyExpenseReportOption(report: Report & {selected?: boolean; searchText?: string}) { + const expenseReport = policyExpenseReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]; const policyExpenseChatAvatarSource = ReportUtils.getWorkspaceAvatar(expenseReport); const reportName = ReportUtils.getReportName(expenseReport); return { ...expenseReport, - keyForList: expenseReport.policyID, + keyForList: expenseReport?.policyID, text: reportName, alternateText: Localize.translateLocal('workspace.common.workspace'), icons: [ @@ -120,35 +173,27 @@ function getPolicyExpenseReportOption(report) { /** * Adds expensify SMS domain (@expensify.sms) if login is a phone number and if it's not included yet - * - * @param {String} login - * @return {String} */ -function addSMSDomainIfPhoneNumber(login) { +function addSMSDomainIfPhoneNumber(login: string): string { const parsedPhoneNumber = parsePhoneNumber(login); if (parsedPhoneNumber.possible && !Str.isValidEmail(login)) { - return parsedPhoneNumber.number.e164 + CONST.SMS.DOMAIN; + return parsedPhoneNumber.number?.e164 + CONST.SMS.DOMAIN; } return login; } /** * Returns avatar data for a list of user accountIDs - * - * @param {Array} accountIDs - * @param {Object} personalDetails - * @param {Object} defaultValues {login: accountID} In workspace invite page, when new user is added we pass available data to opt in - * @returns {Object} */ -function getAvatarsForAccountIDs(accountIDs, personalDetails, defaultValues = {}) { - const reversedDefaultValues = {}; - _.map(Object.entries(defaultValues), (item) => { +function getAvatarsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection, defaultValues: Record = {}) { + const reversedDefaultValues: Record = {}; + + Object.entries(defaultValues).forEach((item) => { reversedDefaultValues[item[1]] = item[0]; }); - - return _.map(accountIDs, (accountID) => { - const login = lodashGet(reversedDefaultValues, accountID, ''); - const userPersonalDetail = lodashGet(personalDetails, accountID, {login, accountID, avatar: ''}); + return accountIDs.map((accountID) => { + const login = reversedDefaultValues[accountID] ?? ''; + const userPersonalDetail = personalDetails[accountID] ?? {login, accountID, avatar: ''}; return { id: accountID, @@ -161,19 +206,16 @@ function getAvatarsForAccountIDs(accountIDs, personalDetails, defaultValues = {} /** * Returns the personal details for an array of accountIDs - * - * @param {Array} accountIDs - * @param {Object} personalDetails - * @returns {Object} – keys of the object are emails, values are PersonalDetails objects. + * @returns keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs, personalDetails) { - const personalDetailsForAccountIDs = {}; +function getPersonalDetailsForAccountIDs(accountIDs: number[], personalDetails: PersonalDetailsCollection) { + const personalDetailsForAccountIDs: Record> = {}; if (!personalDetails) { return personalDetailsForAccountIDs; } - _.each(accountIDs, (accountID) => { + accountIDs?.forEach((accountID) => { const cleanAccountID = Number(accountID); - let personalDetail = personalDetails[accountID]; + let personalDetail: Partial = personalDetails[accountID]; if (!personalDetail) { personalDetail = { avatar: UserUtils.getDefaultAvatar(cleanAccountID), @@ -192,40 +234,36 @@ function getPersonalDetailsForAccountIDs(accountIDs, personalDetails) { /** * Return true if personal details data is ready, i.e. report list options can be created. - * @param {Object} personalDetails - * @returns {Boolean} */ -function isPersonalDetailsReady(personalDetails) { - return !_.isEmpty(personalDetails) && _.some(_.keys(personalDetails), (key) => personalDetails[key].accountID); +function isPersonalDetailsReady(personalDetails: PersonalDetailsCollection): boolean { + const personalDetailsKeys = Object.keys(personalDetails ?? {}); + return personalDetailsKeys.length > 0 && personalDetailsKeys.some((key) => personalDetails[Number(key)].accountID); } /** * Get the participant option for a report. - * @param {Object} participant - * @param {Array} personalDetails - * @returns {Object} */ -function getParticipantsOption(participant, personalDetails) { +function getParticipantsOption(participant: Participant & {searchText?: string}, personalDetails: PersonalDetailsCollection) { const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; - const login = detail.login || participant.login; - const displayName = detail.displayName || LocalePhoneNumber.formatPhoneNumber(login); + const login = detail.login ?? participant.login ?? ''; + const displayName = detail.displayName ?? LocalePhoneNumber.formatPhoneNumber(login); return { keyForList: String(detail.accountID), login, accountID: detail.accountID, text: displayName, - firstName: lodashGet(detail, 'firstName', ''), - lastName: lodashGet(detail, 'lastName', ''), + firstName: detail.firstName ?? '', + lastName: detail.lastName ?? '', alternateText: LocalePhoneNumber.formatPhoneNumber(login) || displayName, icons: [ { - source: UserUtils.getAvatar(detail.avatar, detail.accountID), + source: UserUtils.getAvatar(detail.avatar ?? '', detail.accountID ?? 0), name: login, type: CONST.ICON_TYPE_AVATAR, id: detail.accountID, }, ], - phoneNumber: lodashGet(detail, 'phoneNumber', ''), + phoneNumber: detail.phoneNumber ?? '', selected: participant.selected, searchText: participant.searchText, }; @@ -234,15 +272,12 @@ function getParticipantsOption(participant, personalDetails) { /** * Constructs a Set with all possible names (displayName, firstName, lastName, email) for all participants in a report, * to be used in isSearchStringMatch. - * - * @param {Array} personalDetailList - * @return {Set} */ -function getParticipantNames(personalDetailList) { +function getParticipantNames(personalDetailList?: Array> | null): Set { // We use a Set because `Set.has(value)` on a Set of with n entries is up to n (or log(n)) times faster than // `_.contains(Array, value)` for an Array with n members. - const participantNames = new Set(); - _.each(personalDetailList, (participant) => { + const participantNames = new Set(); + personalDetailList?.forEach((participant) => { if (participant.login) { participantNames.add(participant.login.toLowerCase()); } @@ -262,21 +297,19 @@ function getParticipantNames(personalDetailList) { /** * A very optimized method to remove duplicates from an array. * Taken from https://stackoverflow.com/a/9229821/9114791 - * - * @param {Array} items - * @returns {Array} */ -function uniqFast(items) { - const seenItems = {}; - const result = []; +function uniqFast(items: string[]) { + const seenItems: Record = {}; + const result: string[] = []; let j = 0; - for (let i = 0; i < items.length; i++) { - const item = items[i]; + + for (const item of items) { if (seenItems[item] !== 1) { seenItems[item] = 1; result[j++] = item; } } + return result; } @@ -287,26 +320,18 @@ function uniqFast(items) { * This method must be incredibly performant. It was found to be a big performance bottleneck * when dealing with accounts that have thousands of reports. For loops are more efficient than _.each * Array.prototype.push.apply is faster than using the spread operator, and concat() is faster than push(). - * - * @param {Object} report - * @param {String} reportName - * @param {Array} personalDetailList - * @param {Boolean} isChatRoomOrPolicyExpenseChat - * @param {Boolean} isThread - * @return {String} + */ -function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolicyExpenseChat, isThread) { - let searchTerms = []; +function getSearchText(report: Report, reportName: string, personalDetailList: Array>, isChatRoomOrPolicyExpenseChat: boolean, isThread: boolean): string { + let searchTerms: string[] = []; if (!isChatRoomOrPolicyExpenseChat) { - for (let i = 0; i < personalDetailList.length; i++) { - const personalDetail = personalDetailList[i]; - + for (const personalDetail of personalDetailList) { if (personalDetail.login) { // The regex below is used to remove dots only from the local part of the user email (local-part@domain) // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) // More info https://github.com/Expensify/App/issues/8007 - searchTerms = searchTerms.concat([personalDetail.displayName, personalDetail.login, personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '')]); + searchTerms = searchTerms.concat([personalDetail.displayName ?? '', personalDetail.login, personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '')]); } } } @@ -324,12 +349,13 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic Array.prototype.push.apply(searchTerms, chatRoomSubtitle.split(/[,\s]/)); } else { - const participantAccountIDs = report.participantAccountIDs || []; - for (let i = 0; i < participantAccountIDs.length; i++) { - const accountID = participantAccountIDs[i]; - - if (allPersonalDetails[accountID] && allPersonalDetails[accountID].login) { - searchTerms = searchTerms.concat(allPersonalDetails[accountID].login); + const participantAccountIDs = report.participantAccountIDs ?? []; + if (allPersonalDetails) { + for (const accountID of participantAccountIDs) { + const login = allPersonalDetails[accountID]?.login; + if (login) { + searchTerms.push(login); + } } } } @@ -340,24 +366,21 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic /** * Get an object of error messages keyed by microtime by combining all error objects related to the report. - * @param {Object} report - * @param {Object} reportActions - * @returns {Object} */ -function getAllReportErrors(report, reportActions) { - const reportErrors = report.errors || {}; - const reportErrorFields = report.errorFields || {}; - const reportActionErrors = {}; - _.each(reportActions, (action) => { - if (action && !_.isEmpty(action.errors)) { - _.extend(reportActionErrors, action.errors); +function getAllReportErrors(report: Report, reportActions: Record) { + const reportErrors = report.errors ?? {}; + const reportErrorFields = report.errorFields ?? {}; + const reportActionErrors: OnyxCommon.Errors = {}; + Object.values(reportActions ?? {}).forEach((action) => { + if (action && Object.keys(action.errors ?? {}).length > 0) { + Object.assign(reportActionErrors, action.errors); } else if (ReportActionUtils.isReportPreviewAction(action)) { const iouReportID = ReportActionUtils.getIOUReportIDFromReportActionPreview(action); // Instead of adding all Smartscan errors, let's just add a generic error if there are any. This // will be more performant and provide the same result in the UI if (ReportUtils.hasMissingSmartscanFields(iouReportID)) { - _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + Object.assign(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); } } }); @@ -368,27 +391,27 @@ function getAllReportErrors(report, reportActions) { ...reportErrorFields, reportActionErrors, }; - // Combine all error messages keyed by microtime into one object - const allReportErrors = _.reduce(errorSources, (prevReportErrors, errors) => (_.isEmpty(errors) ? prevReportErrors : _.extend(prevReportErrors, errors)), {}); + const allReportErrors = Object.values(errorSources)?.reduce( + (prevReportErrors, errors) => (Object.keys(errors ?? {}).length > 0 ? prevReportErrors : Object.assign(prevReportErrors, errors)), + {}, + ); return allReportErrors; } /** * Get the last message text from the report directly or from other sources for special cases. - * @param {Object} report - * @returns {String} */ -function getLastMessageTextForReport(report) { - const lastReportAction = _.find( - allSortedReportActions[report.reportID], - (reportAction, key) => ReportActionUtils.shouldReportActionBeVisible(reportAction, key) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, +function getLastMessageTextForReport(report: Report) { + const lastReportAction = allSortedReportActions[report.reportID ?? '']?.find( + (reportAction, key) => ReportActionUtils.shouldReportActionBeVisible(reportAction, String(key)) && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, ); + let lastMessageTextFromReport = ''; - if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { - lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; + if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText ?? '', html: report.lastMessageHtml ?? '', translationKey: report.lastMessageTranslationKey ?? ''})) { + lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey ?? 'common.attachment')}]`; } else if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(report, lastReportAction, true); } else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) { @@ -398,18 +421,16 @@ function getLastMessageTextForReport(report) { const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else { - lastMessageTextFromReport = report ? report.lastMessageText || '' : ''; + lastMessageTextFromReport = report ? report.lastMessageText ?? '' : ''; // Yeah this is a bit ugly. If the latest report action that is not a whisper has been moderated as pending remove // then set the last message text to the text of the latest visible action that is not a whisper or the report creation message. - const lastNonWhisper = _.find(allSortedReportActions[report.reportID], (action) => !ReportActionUtils.isWhisperAction(action)) || {}; + const lastNonWhisper = allSortedReportActions[report.reportID ?? '']?.find((action) => !ReportActionUtils.isWhisperAction(action)) ?? {}; if (ReportActionUtils.isPendingRemove(lastNonWhisper)) { - const latestVisibleAction = - _.find( - allSortedReportActions[report.reportID], - (action) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(action) && !ReportActionUtils.isCreatedAction(action), - ) || {}; - lastMessageTextFromReport = lodashGet(latestVisibleAction, 'message[0].text', ''); + const latestVisibleAction: ReportAction | undefined = allSortedReportActions[report.reportID ?? ''].find( + (action) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(action) && !ReportActionUtils.isCreatedAction(action), + ); + lastMessageTextFromReport = latestVisibleAction?.message?.[0].text ?? ''; } } return lastMessageTextFromReport; @@ -417,18 +438,15 @@ function getLastMessageTextForReport(report) { /** * Creates a report list option - * - * @param {Array} accountIDs - * @param {Object} personalDetails - * @param {Object} report - * @param {Object} reportActions - * @param {Object} options - * @param {Boolean} [options.showChatPreviewLine] - * @param {Boolean} [options.forcePolicyNamePreview] - * @returns {Object} */ -function createOption(accountIDs, personalDetails, report, reportActions = {}, {showChatPreviewLine = false, forcePolicyNamePreview = false}) { - const result = { +function createOption( + accountIDs: number[], + personalDetails: PersonalDetailsCollection, + report: Report, + reportActions: Record, + {showChatPreviewLine = false, forcePolicyNamePreview = false}: {showChatPreviewLine?: boolean; forcePolicyNamePreview?: boolean}, +) { + const result: Option = { text: null, alternateText: null, pendingAction: null, @@ -462,8 +480,8 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { }; const personalDetailMap = getPersonalDetailsForAccountIDs(accountIDs, personalDetails); - const personalDetailList = _.values(personalDetailMap); - const personalDetail = personalDetailList[0] || {}; + const personalDetailList = Object.values(personalDetailMap); + const personalDetail = personalDetailList[0] ?? {}; let hasMultipleParticipants = personalDetailList.length > 1; let subtitle; let reportName; @@ -482,7 +500,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.allReportErrors = getAllReportErrors(report, reportActions); result.brickRoadIndicator = !_.isEmpty(result.allReportErrors) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; - result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; + result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom ?? report.pendingFields.createChat : null; result.ownerAccountID = report.ownerAccountID; result.reportID = report.reportID; result.isUnread = ReportUtils.isUnread(report); @@ -490,7 +508,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); - result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs || []); + result.tooltipText = ReportUtils.getReportParticipantsTitle(report.participantAccountIDs ?? []); result.hasOutstandingIOU = report.hasOutstandingIOU; result.isWaitingOnBankAccount = report.isWaitingOnBankAccount; result.policyID = report.policyID; @@ -499,16 +517,14 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { subtitle = ReportUtils.getChatRoomSubtitle(report); const lastMessageTextFromReport = getLastMessageTextForReport(report); - const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; + const lastActorDetails = personalDetailMap[report.lastActorAccountID ?? 0] ?? null; let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isArchivedRoom) { - const archiveReason = - (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || - CONST.REPORT.ARCHIVE_REASON.DEFAULT; + const archiveReason = lastReportActions[report.reportID ?? ''].originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: archiveReason.displayName ?? PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), policyName: ReportUtils.getPolicyName(report), }); } @@ -526,7 +542,8 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } else { reportName = ReportUtils.getDisplayNameForParticipant(accountIDs[0]); result.keyForList = String(accountIDs[0]); - result.alternateText = LocalePhoneNumber.formatPhoneNumber(lodashGet(personalDetails, [accountIDs[0], 'login'], '')); + + result.alternateText = LocalePhoneNumber.formatPhoneNumber(personalDetails[accountIDs[0]].login ?? ''); } result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result); @@ -539,8 +556,8 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { } result.text = reportName; - result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); - result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), personalDetail.login, personalDetail.accountID); + result.searchText = getSearchText(report, reportName, personalDetailList, result.isChatRoom ?? result.isPolicyExpenseChat, result.isThread); + result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar ?? '', personalDetail.accountID), personalDetail.login, personalDetail.accountID); result.subtitle = subtitle; return result; @@ -548,16 +565,10 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { /** * Searches for a match when provided with a value - * - * @param {String} searchValue - * @param {String} searchText - * @param {Set} [participantNames] - * @param {Boolean} isChatRoom - * @returns {Boolean} */ -function isSearchStringMatch(searchValue, searchText, participantNames = new Set(), isChatRoom = false) { +function isSearchStringMatch(searchValue: string, searchText?: string | null, participantNames = new Set(), isChatRoom = false): boolean { const searchWords = new Set(searchValue.replace(/,/g, ' ').split(' ')); - const valueToSearch = searchText && searchText.replace(new RegExp(/ /g), ''); + const valueToSearch = searchText?.replace(new RegExp(/ /g), ''); let matching = true; searchWords.forEach((word) => { // if one of the word is not matching, we don't need to check further @@ -565,7 +576,7 @@ function isSearchStringMatch(searchValue, searchText, participantNames = new Set return; } const matchRegex = new RegExp(Str.escapeForRegExp(word), 'i'); - matching = matchRegex.test(valueToSearch) || (!isChatRoom && participantNames.has(word)); + matching = matchRegex.test(valueToSearch ?? '') || (!isChatRoom && participantNames.has(word)); }); return matching; } @@ -574,69 +585,59 @@ function isSearchStringMatch(searchValue, searchText, participantNames = new Set * Checks if the given userDetails is currentUser or not. * Note: We can't migrate this off of using logins because this is used to check if you're trying to start a chat with * yourself or a different user, and people won't be starting new chats via accountID usually. - * - * @param {Object} userDetails - * @returns {Boolean} */ -function isCurrentUser(userDetails) { +function isCurrentUser(userDetails: PersonalDetails): boolean { if (!userDetails) { return false; } // If user login is a mobile number, append sms domain if not appended already. - const userDetailsLogin = addSMSDomainIfPhoneNumber(userDetails.login); + const userDetailsLogin = addSMSDomainIfPhoneNumber(userDetails.login ?? ''); - if (currentUserLogin.toLowerCase() === userDetailsLogin.toLowerCase()) { + if (currentUserLogin?.toLowerCase() === userDetailsLogin.toLowerCase()) { return true; } // Check if userDetails login exists in loginList - return _.some(_.keys(loginList), (login) => login.toLowerCase() === userDetailsLogin.toLowerCase()); + return Object.keys(loginList ?? {}).some((login) => login.toLowerCase() === userDetailsLogin.toLowerCase()); } /** * Calculates count of all enabled options - * - * @param {Object[]} options - an initial strings array - * @param {Boolean} options[].enabled - a flag to enable/disable option in a list - * @param {String} options[].name - a name of an option - * @returns {Number} */ -function getEnabledCategoriesCount(options) { - return _.filter(options, (option) => option.enabled).length; +function getEnabledCategoriesCount(options: Record): number { + return Object.values(options).filter((option) => option.enabled).length; } /** * Verifies that there is at least one enabled option - * - * @param {Object[]} options - an initial strings array - * @param {Boolean} options[].enabled - a flag to enable/disable option in a list - * @param {String} options[].name - a name of an option - * @returns {Boolean} */ -function hasEnabledOptions(options) { - return _.some(options, (option) => option.enabled); +function hasEnabledOptions(options: Record): boolean { + return Object.values(options).some((option) => option.enabled); } /** * Build the options for the category tree hierarchy via indents - * - * @param {Object[]} options - an initial object array - * @param {Boolean} options[].enabled - a flag to enable/disable option in a list - * @param {String} options[].name - a name of an option - * @param {Boolean} [isOneLine] - a flag to determine if text should be one line - * @returns {Array} */ -function getCategoryOptionTree(options, isOneLine = false) { - const optionCollection = {}; +function getCategoryOptionTree(options: PolicyCategory[], isOneLine = false) { + const optionCollection: Record< + string, + { + text: string; + keyForList: string; + searchText: string; + tooltipText: string; + isDisabled: boolean; + } + > = {}; - _.each(options, (option) => { + Object.values(options).forEach((option) => { if (!option.enabled) { return; } if (isOneLine) { - if (_.has(optionCollection, option.name)) { + if (Object.prototype.hasOwnProperty.call(optionCollection, option.name)) { return; } @@ -669,28 +670,23 @@ function getCategoryOptionTree(options, isOneLine = false) { }); }); - return _.values(optionCollection); + return Object.values(optionCollection); } /** * Build the section list for categories - * - * @param {Object} categories - * @param {String[]} recentlyUsedCategories - * @param {Object[]} selectedOptions - * @param {String} selectedOptions[].name - * @param {String} searchInputValue - * @param {Number} maxRecentReportsToShow - * @returns {Array} */ -function getCategoryListSections(categories, recentlyUsedCategories, selectedOptions, searchInputValue, maxRecentReportsToShow) { +function getCategoryListSections( + categories: Record, + recentlyUsedCategories: string[], + selectedOptions: PolicyCategory[], + searchInputValue: string, + maxRecentReportsToShow: number, +) { const categorySections = []; - const categoriesValues = _.chain(categories) - .values() - .filter((category) => category.enabled) - .value(); + const categoriesValues = Object.values(categories).filter((category) => category.enabled); - const numberOfCategories = _.size(categoriesValues); + const numberOfCategories = categoriesValues.length; let indexOffset = 0; if (numberOfCategories === 0 && selectedOptions.length > 0) { @@ -705,8 +701,8 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } - if (!_.isEmpty(searchInputValue)) { - const searchCategories = _.filter(categoriesValues, (category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); + if (searchInputValue) { + const searchCategories = categoriesValues.filter((category) => category.name.toLowerCase().includes(searchInputValue.toLowerCase())); categorySections.push({ // "Search" section @@ -731,17 +727,16 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt return categorySections; } - const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); - const filteredRecentlyUsedCategories = _.map( - _.filter(recentlyUsedCategories, (category) => !_.includes(selectedOptionNames, category)), - (category) => ({ + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const filteredRecentlyUsedCategories = recentlyUsedCategories + .filter((category) => !selectedOptionNames.includes(category)) + .map((category) => ({ name: category, enabled: lodashGet(categories, `${category}.enabled`, false), - }), - ); - const filteredCategories = _.filter(categoriesValues, (category) => !_.includes(selectedOptionNames, category.name)); + })); + const filteredCategories = categoriesValues.filter((category) => !selectedOptionNames.includes(category.name)); - if (!_.isEmpty(selectedOptions)) { + if (selectedOptions) { categorySections.push({ // "Selected" section title: '', @@ -780,14 +775,9 @@ function getCategoryListSections(categories, recentlyUsedCategories, selectedOpt /** * Transforms the provided tags into objects with a specific structure. - * - * @param {Object[]} tags - an initial tag array - * @param {Boolean} tags[].enabled - a flag to enable/disable option in a list - * @param {String} tags[].name - a name of an option - * @returns {Array} */ -function getTagsOptions(tags) { - return _.map(tags, (tag) => ({ +function getTagsOptions(tags: Tag[]) { + return tags.map((tag) => ({ text: tag.name, keyForList: tag.name, searchText: tag.name, @@ -798,26 +788,16 @@ function getTagsOptions(tags) { /** * Build the section list for tags - * - * @param {Object[]} tags - * @param {String} tags[].name - * @param {Boolean} tags[].enabled - * @param {String[]} recentlyUsedTags - * @param {Object[]} selectedOptions - * @param {String} selectedOptions[].name - * @param {String} searchInputValue - * @param {Number} maxRecentReportsToShow - * @returns {Array} */ -function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow) { +function getTagListSections(tags: Tag[], recentlyUsedTags: string[], selectedOptions: Array<{name: string; enabled: boolean}>, searchInputValue: string, maxRecentReportsToShow: number) { const tagSections = []; - const enabledTags = _.filter(tags, (tag) => tag.enabled); - const numberOfTags = _.size(enabledTags); + const enabledTags = tags.filter((tag) => tag.enabled); + const numberOfTags = enabledTags.length; let indexOffset = 0; // If all tags are disabled but there's a previously selected tag, show only the selected tag if (numberOfTags === 0 && selectedOptions.length > 0) { - const selectedTagOptions = _.map(selectedOptions, (option) => ({ + const selectedTagOptions = selectedOptions.map((option) => ({ name: option.name, // Should be marked as enabled to be able to be de-selected enabled: true, @@ -833,8 +813,8 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput return tagSections; } - if (!_.isEmpty(searchInputValue)) { - const searchTags = _.filter(enabledTags, (tag) => tag.name.toLowerCase().includes(searchInputValue.toLowerCase())); + if (searchInputValue) { + const searchTags = enabledTags.filter((tag) => tag.name.toLowerCase().includes(searchInputValue.toLowerCase())); tagSections.push({ // "Search" section @@ -859,19 +839,18 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput return tagSections; } - const selectedOptionNames = _.map(selectedOptions, (selectedOption) => selectedOption.name); - const filteredRecentlyUsedTags = _.map( - _.filter(recentlyUsedTags, (recentlyUsedTag) => { - const tagObject = _.find(tags, (tag) => tag.name === recentlyUsedTag); - return Boolean(tagObject && tagObject.enabled) && !_.includes(selectedOptionNames, recentlyUsedTag); - }), - (tag) => ({name: tag, enabled: true}), - ); - const filteredTags = _.filter(enabledTags, (tag) => !_.includes(selectedOptionNames, tag.name)); - - if (!_.isEmpty(selectedOptions)) { - const selectedTagOptions = _.map(selectedOptions, (option) => { - const tagObject = _.find(tags, (tag) => tag.name === option.name); + const selectedOptionNames = selectedOptions.map((selectedOption) => selectedOption.name); + const filteredRecentlyUsedTags = recentlyUsedTags + .filter((recentlyUsedTag) => { + const tagObject = tags.find((tag) => tag.name === recentlyUsedTag); + return Boolean(tagObject && tagObject.enabled) && !selectedOptionNames.includes(recentlyUsedTag); + }) + .map((tag) => ({name: tag, enabled: true})); + const filteredTags = enabledTags.filter((tag) => !selectedOptionNames.includes(tag.name)); + + if (selectedOptions) { + const selectedTagOptions = selectedOptions.map((option) => { + const tagObject = tags.find((tag) => tag.name === option.name); return { name: option.name, enabled: Boolean(tagObject && tagObject.enabled), @@ -916,16 +895,10 @@ function getTagListSections(tags, recentlyUsedTags, selectedOptions, searchInput /** * Build the options - * - * @param {Object} reports - * @param {Object} personalDetails - * @param {Object} options - * @returns {Object} - * @private */ function getOptions( - reports, - personalDetails, + reports: Record, + personalDetails: PersonalDetailsCollection, { reportActions = {}, betas = [], @@ -954,6 +927,34 @@ function getOptions( tags = {}, recentlyUsedTags = [], canInviteUser = true, + }: { + betas: Beta[]; + reportActions?: Record; + selectedOptions?: any[]; + maxRecentReportsToShow?: number; + excludeLogins?: any[]; + includeMultipleParticipantReports?: boolean; + includePersonalDetails?: boolean; + includeRecentReports?: boolean; + // When sortByReportTypeInSearch flag is true, recentReports will include the personalDetails options as well. + sortByReportTypeInSearch?: boolean; + searchInputValue?: string; + showChatPreviewLine?: boolean; + sortPersonalDetailsByAlphaAsc?: boolean; + forcePolicyNamePreview?: boolean; + includeOwnedWorkspaceChats?: boolean; + includeThreads?: boolean; + includeTasks?: boolean; + includeMoneyRequests?: boolean; + excludeUnknownUsers?: boolean; + includeP2P?: boolean; + includeCategories?: boolean; + categories?: Record; + recentlyUsedCategories?: any[]; + includeTags?: boolean; + tags?: Record; + recentlyUsedTags?: any[]; + canInviteUser?: boolean; }, ) { if (includeCategories) { @@ -970,7 +971,7 @@ function getOptions( } if (includeTags) { - const tagOptions = getTagListSections(_.values(tags), recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow); + const tagOptions = getTagListSections(Object.values(tags), recentlyUsedTags, selectedOptions, searchInputValue, maxRecentReportsToShow); return { recentReports: [], @@ -994,13 +995,13 @@ function getOptions( } let recentReportOptions = []; - let personalDetailsOptions = []; - const reportMapForAccountIDs = {}; + let personalDetailsOptions: Option[] = []; + const reportMapForAccountIDs: Record = {}; const parsedPhoneNumber = parsePhoneNumber(LoginUtils.appendCountryCode(Str.removeSMSDomain(searchInputValue))); - const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number.e164 : searchInputValue.toLowerCase(); + const searchValue = parsedPhoneNumber.possible ? parsedPhoneNumber.number?.e164 : searchInputValue.toLowerCase(); // Filter out all the reports that shouldn't be displayed - const filteredReports = _.filter(reports, (report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getTopmostReportId(), false, betas, policies)); + const filteredReports = Object.values(reports).filter((report) => ReportUtils.shouldReportBeInOptionList(report, Navigation.getTopmostReportId(), false, betas, policies)); // Sorting the reports works like this: // - Order everything by the last message timestamp (descending) @@ -1014,8 +1015,8 @@ function getOptions( }); orderedReports.reverse(); - const allReportOptions = []; - _.each(orderedReports, (report) => { + const allReportOptions: Option[] = []; + orderedReports.forEach((report) => { if (!report) { return; } @@ -1025,7 +1026,7 @@ function getOptions( const isTaskReport = ReportUtils.isTaskReport(report); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isMoneyRequestReport = ReportUtils.isMoneyRequestReport(report); - const accountIDs = report.participantAccountIDs || []; + const accountIDs = report.participantAccountIDs ?? []; if (isPolicyExpenseChat && report.isOwnPolicyExpenseChat && !includeOwnedWorkspaceChats) { return; @@ -1072,14 +1073,13 @@ function getOptions( }), ); }); - // We're only picking personal details that have logins set // This is a temporary fix for all the logic that's been breaking because of the new privacy changes // See https://github.com/Expensify/Expensify/issues/293465 for more context // Moreover, we should not override the personalDetails object, otherwise the createOption util won't work properly, it returns incorrect tooltipText const havingLoginPersonalDetails = !includeP2P ? {} : _.pick(personalDetails, (detail) => Boolean(detail.login)); - let allPersonalDetailsOptions = _.map(havingLoginPersonalDetails, (personalDetail) => - createOption([personalDetail.accountID], personalDetails, reportMapForAccountIDs[personalDetail.accountID], reportActions, { + let allPersonalDetailsOptions = Object.values(havingLoginPersonalDetails).map((personalDetail) => + createOption([personalDetail?.accountID ?? 0], personalDetails, reportMapForAccountIDs[personalDetail?.accountID], reportActions, { showChatPreviewLine, forcePolicyNamePreview, }), @@ -1087,20 +1087,18 @@ function getOptions( if (sortPersonalDetailsByAlphaAsc) { // PersonalDetails should be ordered Alphabetically by default - https://github.com/Expensify/App/issues/8220#issuecomment-1104009435 - allPersonalDetailsOptions = lodashOrderBy(allPersonalDetailsOptions, [(personalDetail) => personalDetail.text && personalDetail.text.toLowerCase()], 'asc'); + allPersonalDetailsOptions = lodashOrderBy(allPersonalDetailsOptions, [(personalDetail) => personalDetail.text?.toLowerCase()], 'asc'); } // Always exclude already selected options and the currently logged in user const optionsToExclude = [...selectedOptions, {login: currentUserLogin}]; - _.each(excludeLogins, (login) => { + excludeLogins.forEach((login) => { optionsToExclude.push({login}); }); if (includeRecentReports) { - for (let i = 0; i < allReportOptions.length; i++) { - const reportOption = allReportOptions[i]; - + for (const reportOption of allReportOptions) { // Stop adding options to the recentReports array when we reach the maxRecentReportsToShow value if (recentReportOptions.length > 0 && recentReportOptions.length === maxRecentReportsToShow) { break; @@ -1117,8 +1115,8 @@ function getOptions( // If we're excluding threads, check the report to see if it has a single participant and if the participant is already selected if ( !includeThreads && - (reportOption.login || reportOption.reportID) && - _.some(optionsToExclude, (option) => (option.login && option.login === reportOption.login) || (option.reportID && option.reportID === reportOption.reportID)) + (reportOption.login ?? reportOption.reportID) && + optionsToExclude.some((option) => (option.login && option.login === reportOption.login) ?? option.reportID === reportOption.reportID) ) { continue; } @@ -1129,7 +1127,7 @@ function getOptions( if (searchValue) { // Determine if the search is happening within a chat room and starts with the report ID - const isReportIdSearch = isChatRoom && Str.startsWith(reportOption.reportID, searchValue); + const isReportIdSearch = isChatRoom && Str.startsWith(reportOption.reportID ?? '', searchValue); // Check if the search string matches the search text or participant names considering the type of the room const isSearchMatch = isSearchStringMatch(searchValue, searchText, participantNames, isChatRoom); @@ -1150,8 +1148,8 @@ function getOptions( if (includePersonalDetails) { // Next loop over all personal details removing any that are selectedUsers or recentChats - _.each(allPersonalDetailsOptions, (personalDetailOption) => { - if (_.some(optionsToExclude, (optionToExclude) => optionToExclude.login === personalDetailOption.login)) { + allPersonalDetailsOptions.forEach((personalDetailOption) => { + if (optionsToExclude.some((optionToExclude) => optionToExclude.login === personalDetailOption.login)) { return; } const {searchText, participantsList, isChatRoom} = personalDetailOption; @@ -1163,23 +1161,23 @@ function getOptions( }); } - let currentUserOption = _.find(allPersonalDetailsOptions, (personalDetailsOption) => personalDetailsOption.login === currentUserLogin); + let currentUserOption = allPersonalDetailsOptions.find((personalDetailsOption) => personalDetailsOption.login === currentUserLogin); if (searchValue && currentUserOption && !isSearchStringMatch(searchValue, currentUserOption.searchText)) { currentUserOption = null; } let userToInvite = null; const noOptions = recentReportOptions.length + personalDetailsOptions.length === 0 && !currentUserOption; - const noOptionsMatchExactly = !_.find(personalDetailsOptions.concat(recentReportOptions), (option) => option.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()); + const noOptionsMatchExactly = !personalDetailsOptions.concat(recentReportOptions).find((option) => option.login === addSMSDomainIfPhoneNumber(searchValue ?? '').toLowerCase()); if ( searchValue && (noOptions || noOptionsMatchExactly) && !isCurrentUser({login: searchValue}) && - _.every(selectedOptions, (option) => option.login !== searchValue) && + selectedOptions.every((option) => option.login !== searchValue) && ((Str.isValidEmail(searchValue) && !Str.isDomainEmail(searchValue) && !Str.endsWith(searchValue, CONST.SMS.DOMAIN)) || - (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number.input)))) && - !_.find(optionsToExclude, (optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && + (parsedPhoneNumber.possible && Str.isValidPhone(LoginUtils.getPhoneNumberWithoutSpecialChars(parsedPhoneNumber.number?.input ?? '')))) && + !optionsToExclude.find((optionToExclude) => optionToExclude.login === addSMSDomainIfPhoneNumber(searchValue).toLowerCase()) && (searchValue !== CONST.EMAIL.CHRONOS || Permissions.canUseChronos(betas)) && !excludeUnknownUsers ) { @@ -1198,8 +1196,8 @@ function getOptions( }); userToInvite.isOptimisticAccount = true; userToInvite.login = searchValue; - userToInvite.text = userToInvite.text || searchValue; - userToInvite.alternateText = userToInvite.alternateText || searchValue; + userToInvite.text = userToInvite.text ?? searchValue; + userToInvite.alternateText = userToInvite.alternateText ?? searchValue; // If user doesn't exist, use a default avatar userToInvite.icons = [ @@ -1226,7 +1224,7 @@ function getOptions( if (!option.login) { return 2; } - if (option.login.toLowerCase() !== searchValue.toLowerCase()) { + if (option.login.toLowerCase() !== searchValue?.toLowerCase()) { return 1; } @@ -1250,14 +1248,8 @@ function getOptions( /** * Build the options for the Search view - * - * @param {Object} reports - * @param {Object} personalDetails - * @param {String} searchValue - * @param {Array} betas - * @returns {Object} */ -function getSearchOptions(reports, personalDetails, searchValue = '', betas) { +function getSearchOptions(reports: Record, personalDetails: PersonalDetailsCollection, betas: Beta[] = [], searchValue = '') { return getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), @@ -1277,13 +1269,9 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { /** * Build the IOUConfirmation options for showing the payee personalDetail - * - * @param {Object} personalDetail - * @param {String} amountText - * @returns {Object} */ -function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amountText) { - const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); +function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail: PersonalDetails, amountText: string) { + const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login ?? ''); return { text: personalDetail.displayName || formattedLogin, alternateText: formattedLogin || personalDetail.displayName, @@ -1303,13 +1291,9 @@ function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amount /** * Build the IOUConfirmationOptions for showing participants - * - * @param {Array} participants - * @param {String} amountText - * @returns {Array} */ -function getIOUConfirmationOptionsFromParticipants(participants, amountText) { - return _.map(participants, (participant) => ({ +function getIOUConfirmationOptionsFromParticipants(participants: Option[], amountText: string) { + return participants.map((participant) => ({ ...participant, descriptiveText: amountText, })); @@ -1317,28 +1301,11 @@ function getIOUConfirmationOptionsFromParticipants(participants, amountText) { /** * Build the options for the New Group view - * - * @param {Object} reports - * @param {Object} personalDetails - * @param {Array} [betas] - * @param {String} [searchValue] - * @param {Array} [selectedOptions] - * @param {Array} [excludeLogins] - * @param {Boolean} [includeOwnedWorkspaceChats] - * @param {boolean} [includeP2P] - * @param {boolean} [includeCategories] - * @param {Object} [categories] - * @param {Array} [recentlyUsedCategories] - * @param {boolean} [includeTags] - * @param {Object} [tags] - * @param {Array} [recentlyUsedTags] - * @param {boolean} [canInviteUser] - * @returns {Object} */ function getFilteredOptions( - reports, - personalDetails, - betas = [], + reports: Record, + personalDetails: PersonalDetailsCollection, + betas: Beta[] = [], searchValue = '', selectedOptions = [], excludeLogins = [], @@ -1374,22 +1341,12 @@ function getFilteredOptions( /** * Build the options for the Share Destination for a Task - * * - * @param {Object} reports - * @param {Object} personalDetails - * @param {Array} [betas] - * @param {String} [searchValue] - * @param {Array} [selectedOptions] - * @param {Array} [excludeLogins] - * @param {Boolean} [includeOwnedWorkspaceChats] - * @returns {Object} - * */ function getShareDestinationOptions( - reports, - personalDetails, - betas = [], + reports: Record, + personalDetails: PersonalDetailsCollection, + betas: Beta[] = [], searchValue = '', selectedOptions = [], excludeLogins = [], @@ -1418,30 +1375,29 @@ function getShareDestinationOptions( /** * Format personalDetails or userToInvite to be shown in the list * - * @param {Object} member - personalDetails or userToInvite - * @param {Boolean} isSelected - whether the item is selected - * @returns {Object} + * @param member - personalDetails or userToInvite + * @param isSelected - whether the item is selected */ -function formatMemberForList(member, isSelected) { +function formatMemberForList(member: Option | PersonalDetails, isSelected: boolean) { if (!member) { return undefined; } - const avatarSource = lodashGet(member, 'participantsList[0].avatar', '') || lodashGet(member, 'avatar', ''); - const accountID = lodashGet(member, 'accountID', ''); + const avatarSource = member.participantsList?.[0]?.avatar ?? member.avatar ?? ''; + const accountID = member.accountID; return { - text: lodashGet(member, 'text', '') || lodashGet(member, 'displayName', ''), - alternateText: lodashGet(member, 'alternateText', '') || lodashGet(member, 'login', ''), - keyForList: lodashGet(member, 'keyForList', '') || String(accountID), + text: member.text ?? member.displayName ?? '', + alternateText: member.alternateText ?? member.login ?? '', + keyForList: member.keyForList ?? String(accountID), isSelected, isDisabled: false, accountID, - login: lodashGet(member, 'login', ''), + login: member.login ?? '', rightElement: null, avatar: { source: UserUtils.getAvatar(avatarSource, accountID), - name: lodashGet(member, 'participantsList[0].login', '') || lodashGet(member, 'displayName', ''), + name: member.participantsList?.[0]?.login ?? member.displayName ?? '', type: 'avatar', }, pendingAction: lodashGet(member, 'pendingAction'), @@ -1450,15 +1406,9 @@ function formatMemberForList(member, isSelected) { /** * Build the options for the Workspace Member Invite view - * - * @param {Object} personalDetails - * @param {Array} betas - * @param {String} searchValue - * @param {Array} excludeLogins - * @returns {Object} */ -function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = []) { - return getOptions([], personalDetails, { +function getMemberInviteOptions(personalDetails: PersonalDetailsCollection, betas = [], searchValue = '', excludeLogins = []) { + return getOptions({}, personalDetails, { betas, searchInputValue: searchValue.trim(), includePersonalDetails: true, @@ -1469,15 +1419,8 @@ function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', e /** * Helper method that returns the text to be used for the header's message and title (if any) - * - * @param {Boolean} hasSelectableOptions - * @param {Boolean} hasUserToInvite - * @param {String} searchValue - * @param {Boolean} [maxParticipantsReached] - * @param {Boolean} [hasMatchedParticipant] - * @return {String} */ -function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, maxParticipantsReached = false, hasMatchedParticipant = false) { +function getHeaderMessage(hasSelectableOptions: boolean, hasUserToInvite: boolean, searchValue: string, maxParticipantsReached = false, hasMatchedParticipant = false): string { if (maxParticipantsReached) { return Localize.translate(preferredLocale, 'common.maxParticipantsReached', {count: CONST.REPORT.MAXIMUM_PARTICIPANTS}); } @@ -1510,11 +1453,9 @@ function getHeaderMessage(hasSelectableOptions, hasUserToInvite, searchValue, ma /** * Helper method to check whether an option can show tooltip or not - * @param {Object} option - * @returns {Boolean} */ -function shouldOptionShowTooltip(option) { - return (!option.isChatRoom || option.isThread) && !option.isArchivedRoom; +function shouldOptionShowTooltip(option: Option): boolean { + return Boolean((!option.isChatRoom || option.isThread) && !option.isArchivedRoom); } export { diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index 7151bb84d1f1..ef60e3e90536 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -23,3 +23,4 @@ type IOU = { }; export default IOU; +export type {Participant}; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 46e51fe41238..3468211acb2d 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -77,6 +77,12 @@ type Report = { participantAccountIDs?: number[]; total?: number; currency?: string; + errors?: OnyxCommon.Errors; + errorFields?: OnyxCommon.ErrorFields; + lastMessageTranslationKey?: string; + isWaitingOnBankAccount?: boolean; + iouReportID?: string | number; + pendingFields?: OnyxCommon.ErrorFields; }; export default Report; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index ec505a7e8d07..924d747a2b1a 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -81,6 +81,7 @@ type ReportActionBase = { childVisibleActionCount?: number; pendingAction?: OnyxCommon.PendingAction; + errors?: OnyxCommon.Errors; }; type ReportAction = ReportActionBase & OriginalMessage; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e50925e7adf2..57030a6c68f2 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -1,7 +1,7 @@ import Account from './Account'; import Request from './Request'; import Credentials from './Credentials'; -import IOU from './IOU'; +import IOU, {Participant} from './IOU'; import Modal from './Modal'; import Network from './Network'; import CustomStatusDraft from './CustomStatusDraft'; @@ -98,4 +98,5 @@ export type { RecentlyUsedCategories, RecentlyUsedTags, PolicyTag, + Participant, }; From d67f9f757dc04203da2d640876f5db7630bf3ea4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 2 Oct 2023 16:59:03 +0200 Subject: [PATCH 0005/1081] fix: removed loadshGet usage --- src/libs/OptionsListUtils.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index fb6bf665afd7..868a0128c219 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -409,7 +409,7 @@ function getLastMessageTextForReport(report: Report) { ); let lastMessageTextFromReport = ''; - const lastActionName = lodashGet(lastReportAction, 'actionName', ''); + const lastActionName = lastReportAction?.actionName ?? ''; if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText ?? '', html: report.lastMessageHtml ?? '', translationKey: report.lastMessageTranslationKey ?? ''})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey ?? 'common.attachment')}]`; @@ -739,7 +739,7 @@ function getCategoryListSections( .filter((category) => !selectedOptionNames.includes(category)) .map((category) => ({ name: category, - enabled: lodashGet(categories, `${category}.enabled`, false), + enabled: categories[`${category}`]?.enabled ?? false, })); const filteredCategories = categoriesValues.filter((category) => !selectedOptionNames.includes(category.name)); @@ -1391,19 +1391,19 @@ function formatMemberForList(member, config = {}) { return undefined; } - const accountID = lodashGet(member, 'accountID', ''); + const accountID = member.accountID; return { - text: lodashGet(member, 'text', '') || lodashGet(member, 'displayName', ''), - alternateText: lodashGet(member, 'alternateText', '') || lodashGet(member, 'login', ''), - keyForList: lodashGet(member, 'keyForList', '') || String(accountID), + text: member.text ?? member.displayName ?? '', + alternateText: member.alternateText ?? member.login ?? '', + keyForList: member.keyForList ?? String(accountID), isSelected: false, isDisabled: false, accountID, login: member.login ?? '', rightElement: null, - icons: lodashGet(member, 'icons'), - pendingAction: lodashGet(member, 'pendingAction'), + icons: member.icons, + pendingAction: member.pendingAction, ...config, }; } From 639136ad63796d54e7c18af31b6cd769fcd4f9e4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 7 Nov 2023 10:38:53 +0530 Subject: [PATCH 0006/1081] fix: do not append whitespace to emoji if whitespace is already present --- .../home/report/ReportActionCompose/ComposerWithSuggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index b306676d476a..d99e93e8f628 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -259,7 +259,7 @@ function ComposerWithSuggestions({ (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); - const isEmojiInserted = diff.length && endIndex > startIndex && EmojiUtils.containsOnlyEmojis(diff); + const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis( isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, preferredSkinTone, From 4d299d6af14a810b62784d8c4debcc8358294bc9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 16:24:43 +0700 Subject: [PATCH 0007/1081] fix: attachment modal is not closed when click notification --- ...bscribeToReportCommentPushNotifications.js | 23 +++++++++++-------- src/libs/actions/Report.js | 11 ++++++++- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js index 04fd34bf6075..84d6c4ef6514 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import Visibility from '@libs/Visibility'; +import * as Modal from '@userActions/Modal'; import ROUTES from '@src/ROUTES'; import backgroundRefresh from './backgroundRefresh'; import PushNotification from './index'; @@ -24,17 +25,19 @@ export default function subscribeToReportCommentPushNotifications() { Log.info('[PushNotification] onSelected() - called', false, {reportID, reportActionID}); Navigation.isNavigationReady().then(() => { - try { - // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back - if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) { - Navigation.goBack(ROUTES.HOME); - } + Modal.close(() => { + try { + // If a chat is visible other than the one we are trying to navigate to, then we need to navigate back + if (Navigation.getActiveRoute().slice(1, 2) === ROUTES.REPORT && !Navigation.isActiveRoute(`r/${reportID}`)) { + Navigation.goBack(ROUTES.HOME); + } - Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - } catch (error) { - Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: error.message}); - } + Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + } catch (error) { + Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: error.message}); + } + }); }); }); } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 1de15c1184cb..9d6688882486 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -22,6 +22,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import Visibility from '@libs/Visibility'; +import * as Modal from '@userActions/Modal'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -1815,7 +1816,15 @@ function showReportActionNotification(reportID, reportAction) { const notificationParams = { report, reportAction, - onClick: () => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)), + onClick: () => { + Modal.close(() => { + const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID); + if (Navigation.isActiveRoute(reportRoute)) { + return; + } + Navigation.navigate(reportRoute); + }); + }, }; if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { LocalNotification.showModifiedExpenseNotification(notificationParams); From e2cc597f44a726885355ec8ebd5923739d2408fc Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 17:04:57 +0700 Subject: [PATCH 0008/1081] fix lint --- src/libs/actions/Report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9d6688882486..672ff1d97cd7 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -22,11 +22,11 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import Visibility from '@libs/Visibility'; -import * as Modal from '@userActions/Modal'; import CONFIG from '@src/CONFIG'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import * as Modal from './Modal'; import * as Session from './Session'; import * as Welcome from './Welcome'; From 6062753ff9e20bae3f6f1ca1623c55c4f63ff1c3 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:06:19 +0530 Subject: [PATCH 0009/1081] fix: whitespace after emoji with skin tone --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index cf1fe0e2ec3c..32ea949c4593 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -224,11 +224,13 @@ function ComposerWithSuggestions({ startIndex = currentIndex; // if text is getting pasted over find length of common suffix and subtract it from new text length + const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); if (selection.end - selection.start > 0) { - const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); endIndex = newText.length - commonSuffixLength; + } else if (commonSuffixLength > 0) { + endIndex = currentIndex + newText.length - prevText.length; } else { - endIndex = currentIndex + (newText.length - prevText.length); + endIndex = currentIndex + newText.length; } } From 4d6412eb31349659cf973616c4e0e266e2914f0d Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:29:28 +0530 Subject: [PATCH 0010/1081] fix: whitespace after short codes right before a word --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 32ea949c4593..4e906bd81f9f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -225,10 +225,8 @@ function ComposerWithSuggestions({ // if text is getting pasted over find length of common suffix and subtract it from new text length const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); - if (selection.end - selection.start > 0) { + if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; - } else if (commonSuffixLength > 0) { - endIndex = currentIndex + newText.length - prevText.length; } else { endIndex = currentIndex + newText.length; } From 276cae20b7f0c44d11fc3118216f0c03b71064ac Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:48:00 +0530 Subject: [PATCH 0011/1081] fix: insert whitespace after emoji --- src/libs/ComposerUtils/index.ts | 6 +- .../ComposerWithSuggestions.js | 83 +++++++++++++++---- 2 files changed, 74 insertions(+), 15 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 5a7da7ca08cf..58e1efa7aa65 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -32,7 +32,11 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo */ function getCommonSuffixLength(str1: string, str2: string): number { let i = 0; - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { + if (str1.length === 0 || str2.length === 0) { + return 0; + } + const minLen = Math.min(str1.length, str2.length); + while (i < minLen && str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { i++; } return i; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index b69e65e854d7..8791c1e7cef9 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -119,6 +119,7 @@ function ComposerWithSuggestions({ return draft; }); const commentRef = useRef(value); + const lastTextRef = useRef(value); const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; @@ -206,6 +207,50 @@ function ComposerWithSuggestions({ [], ); + /** + * Find the newly added characters between the previous text and the new text based on the selection. + * + * @param {string} prevText - The previous text. + * @param {string} newText - The new text. + * @returns {object} An object containing information about the newly added characters. + * @property {number} startIndex - The start index of the newly added characters in the new text. + * @property {number} endIndex - The end index of the newly added characters in the new text. + * @property {string} diff - The newly added characters. + */ + const findNewlyAddedChars = useCallback( + (prevText, newText) => { + let startIndex = -1; + let endIndex = -1; + let currentIndex = 0; + + // Find the first character mismatch with newText + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { + currentIndex++; + } + + if (currentIndex < newText.length) { + startIndex = currentIndex; + + const commonSuffixLength = ComposerUtils.getCommonSuffixLength(prevText, newText); + // if text is getting pasted over find length of common suffix and subtract it from new text length + if (commonSuffixLength > 0 || selection.end - selection.start > 0) { + endIndex = newText.length - commonSuffixLength; + } else { + endIndex = currentIndex + newText.length + } + } + + return { + startIndex, + endIndex, + diff: newText.substring(startIndex, endIndex), + }; + }, + [selection.end, selection.start], + ); + + const insertWhiteSpace = (text, index) => `${text.slice(0, index)} ${text.slice(index)}`; + /** * Update the value of the comment in Onyx * @@ -215,7 +260,13 @@ function ComposerWithSuggestions({ const updateComment = useCallback( (commentValue, shouldDebounceSaveComment) => { raiseIsScrollLikelyLayoutTriggered(); - const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); + const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); + const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); + const {text: newComment, emojis} = EmojiUtils.replaceAndExtractEmojis( + isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, + preferredSkinTone, + preferredLocale, + ); if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (!_.isEmpty(newEmojis)) { @@ -260,13 +311,14 @@ function ComposerWithSuggestions({ } }, [ - debouncedUpdateFrequentlyUsedEmojis, - preferredLocale, + raiseIsScrollLikelyLayoutTriggered, + findNewlyAddedChars, preferredSkinTone, - reportID, + preferredLocale, setIsCommentEmpty, suggestionsRef, - raiseIsScrollLikelyLayoutTriggered, + debouncedUpdateFrequentlyUsedEmojis, + reportID, debouncedSaveReportComment, ], ); @@ -317,14 +369,8 @@ function ComposerWithSuggestions({ * @param {Boolean} shouldAddTrailSpace */ const replaceSelectionWithText = useCallback( - (text, shouldAddTrailSpace = true) => { - const updatedText = shouldAddTrailSpace ? `${text} ` : text; - const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0; - updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText)); - setSelection((prevSelection) => ({ - start: prevSelection.start + text.length + selectionSpaceLength, - end: prevSelection.start + text.length + selectionSpaceLength, - })); + (text) => { + updateComment(ComposerUtils.insertText(commentRef.current, selection, text)); }, [selection, updateComment], ); @@ -448,7 +494,12 @@ function ComposerWithSuggestions({ } focus(); - replaceSelectionWithText(e.key, false); + // Reset cursor to last known location + setSelection((prevSelection) => ({ + start: prevSelection.start + 1, + end: prevSelection.end + 1, + })); + replaceSelectionWithText(e.key); }, [checkComposerVisibility, focus, replaceSelectionWithText], ); @@ -524,6 +575,10 @@ function ComposerWithSuggestions({ [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); + useEffect(() => { + lastTextRef.current = value; + }, [value]); + return ( <> From d7269601b2d90d10ae230144787d376ef5a33f07 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 11:55:53 +0530 Subject: [PATCH 0012/1081] fix: prettier issue --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 8791c1e7cef9..518339828e2a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -236,7 +236,7 @@ function ComposerWithSuggestions({ if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; } else { - endIndex = currentIndex + newText.length + endIndex = currentIndex + newText.length; } } From 80535723c3f0301bcf508100116ea59b837bbfb4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Wed, 8 Nov 2023 12:04:01 +0530 Subject: [PATCH 0013/1081] fix: prevent duplicate character insertion on refocus --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 518339828e2a..6c906246121b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -494,14 +494,14 @@ function ComposerWithSuggestions({ } focus(); + // Reset cursor to last known location setSelection((prevSelection) => ({ start: prevSelection.start + 1, end: prevSelection.end + 1, })); - replaceSelectionWithText(e.key); }, - [checkComposerVisibility, focus, replaceSelectionWithText], + [checkComposerVisibility, focus], ); const blur = useCallback(() => { From f56e0e36ff9a11bbe9dd55a7379d36522b7d8098 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 8 Nov 2023 17:12:29 +0700 Subject: [PATCH 0014/1081] fix conflict --- src/pages/home/ReportScreen.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index fd741f051618..7abf395644f8 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,11 +1,3 @@ -<<<<<<< HEAD -import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import PropTypes from 'prop-types'; -import {View} from 'react-native'; -======= ->>>>>>> main import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; From 0c63dac9d928589456f66e18d529cdf6660d768d Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 8 Nov 2023 17:26:10 +0700 Subject: [PATCH 0015/1081] fix lint --- src/components/AttachmentModal.js | 2 ++ src/libs/Navigation/AppNavigator/AuthScreens.js | 2 +- src/pages/ReportAvatar.js | 2 +- src/pages/settings/Profile/ProfileAvatar.js | 4 ++-- src/pages/workspace/WorkspaceAvatar.js | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index c541b47ea9c4..098723f941e6 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -41,6 +41,8 @@ import * as Illustrations from './Icon/Illustrations'; import Modal from './Modal'; import SafeAreaConsumer from './SafeAreaConsumer'; import transactionPropTypes from './transactionPropTypes'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; /** * Modal render prop component that exposes modal launching triggers that can be used diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index 20971041a0d0..719bad9f2c06 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -326,7 +326,7 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio getComponent={loadReportAttachments} listeners={modalScreenListeners} /> - `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, }), -)(ReportAvatar); \ No newline at end of file +)(ReportAvatar); diff --git a/src/pages/settings/Profile/ProfileAvatar.js b/src/pages/settings/Profile/ProfileAvatar.js index 3faff62231f1..948636c4a02f 100644 --- a/src/pages/settings/Profile/ProfileAvatar.js +++ b/src/pages/settings/Profile/ProfileAvatar.js @@ -2,12 +2,12 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import participantPropTypes from '@components/participantPropTypes'; import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import _ from 'underscore'; const propTypes = { /** React Navigation route */ @@ -60,4 +60,4 @@ export default withOnyx({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, -})(ProfileAvatar); \ No newline at end of file +})(ProfileAvatar); diff --git a/src/pages/workspace/WorkspaceAvatar.js b/src/pages/workspace/WorkspaceAvatar.js index da6f4891b2a5..d3e2f700a353 100644 --- a/src/pages/workspace/WorkspaceAvatar.js +++ b/src/pages/workspace/WorkspaceAvatar.js @@ -78,4 +78,4 @@ export default withOnyx({ isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, -})(WorkspaceAvatar); \ No newline at end of file +})(WorkspaceAvatar); From 30f7f4b450129cbd6936c72ff69dd154dfdf9a19 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 15 Nov 2023 21:36:28 +0700 Subject: [PATCH 0016/1081] get full size avatar --- src/pages/settings/Profile/ProfileAvatar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/ProfileAvatar.js b/src/pages/settings/Profile/ProfileAvatar.js index 948636c4a02f..6fe6ba9c0baa 100644 --- a/src/pages/settings/Profile/ProfileAvatar.js +++ b/src/pages/settings/Profile/ProfileAvatar.js @@ -40,7 +40,7 @@ function ProfileAvatar(props) { { Navigation.goBack(); }} From c781431011d147a1ed27a8a158bfa700e8066b00 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 15 Nov 2023 21:59:51 +0700 Subject: [PATCH 0017/1081] add get full size for all --- src/pages/ReportAvatar.js | 4 +++- src/pages/workspace/WorkspaceAvatar.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportAvatar.js b/src/pages/ReportAvatar.js index b3d14f169853..a40edb2226d9 100644 --- a/src/pages/ReportAvatar.js +++ b/src/pages/ReportAvatar.js @@ -7,6 +7,7 @@ import AttachmentModal from '@components/AttachmentModal'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import reportPropTypes from './reportPropTypes'; @@ -60,11 +61,12 @@ const defaultProps = { function ReportAvatar(props) { const isArchivedRoom = ReportUtils.isArchivedRoom(props.report); const policyName = isArchivedRoom ? props.report.oldPolicyName : lodashGet(props.policy, 'name', ''); + const avatarURL = lodashGet(props.policy, 'avatar', '') || ReportUtils.getDefaultWorkspaceAvatar(policyName); return ( { Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID)); }} diff --git a/src/pages/workspace/WorkspaceAvatar.js b/src/pages/workspace/WorkspaceAvatar.js index d3e2f700a353..8495decb842e 100644 --- a/src/pages/workspace/WorkspaceAvatar.js +++ b/src/pages/workspace/WorkspaceAvatar.js @@ -6,6 +6,7 @@ import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; +import * as UserUtils from '@libs/UserUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -51,11 +52,12 @@ const defaultProps = { }; function WorkspaceAvatar(props) { + const avatarURL = lodashGet(props.policy, 'avatar', '') || ReportUtils.getDefaultWorkspaceAvatar(lodashGet(props.policy, 'name', '')); return ( { Navigation.goBack(ROUTES.WORKSPACE_SETTINGS.getRoute(getPolicyIDFromRoute(props.route))); }} From a917e0644a85e5593624dc9a052b5155c9dcc93e Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Thu, 16 Nov 2023 09:32:54 -0500 Subject: [PATCH 0018/1081] adds violations functions to report utils and implements --- .../ReportActionItem/ReportPreview.js | 2 +- src/libs/ReportUtils.js | 78 +++++++++++++++++++ src/libs/SidebarUtils.ts | 2 +- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 45fe7d42e299..38fc7b008095 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -125,7 +125,7 @@ function ReportPreview(props) { const hasReceipts = transactionsWithReceipts.length > 0; const hasOnlyDistanceRequests = ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID); const isScanning = hasReceipts && ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action); - const hasErrors = hasReceipts && ReportUtils.hasMissingSmartscanFields(props.iouReportID); + const hasErrors = (hasReceipts && ReportUtils.hasMissingSmartscanFields(props.iouReportID)) || ReportUtils.reportHasViolations(props.iouReportID); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(props.iouReportID); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 973ed1f0dfd1..12808ef747a5 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import * as IOU from './actions/IOU'; +import * as CollectionUtils from './CollectionUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; import isReportMessageAttachment from './isReportMessageAttachment'; @@ -81,6 +82,19 @@ Onyx.connect({ callback: (val) => (loginList = val), }); +const transactionViolations = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + callback: (violations, key) => { + if (!key || !violations) { + return; + } + + const transactionID = CollectionUtils.extractCollectionItemID(key); + transactionViolations[transactionID] = violations; + }, +}); + function getChatType(report) { return report ? report.chatType : ''; } @@ -3294,6 +3308,63 @@ function shouldHideReport(report, currentReportId) { return parentReport.reportID !== report.reportID && !isChildReportHasComment; } +/** + * @param {String} transactionID + * @returns {Boolean} + */ + +function transactionHasViolation(transactionID) { + const violations = lodashGet(transactionViolations, transactionID, []); + return _.some(violations, (violation) => violation.type === 'violation'); +} + +/** + * + * @param {Object} report + * @returns {Boolean} + */ + +function transactionThreadHasViolations(report) { + if (!Permissions.canUseViolations()) { + return false; + } + // eslint-disable-next-line es/no-nullish-coalescing-operators + if (!report.parentReportActionID ?? 0) { + return false; + } + + const reportActions = ReportActionsUtils.getAllReportActions(report.reportID); + + const parentReportAction = lodashGet(reportActions, `${report.parentReportID}.${report.parentReportActionID}`); + if (!parentReportAction) { + return false; + } + // eslint-disable-next-line es/no-nullish-coalescing-operators + const transactionID = parentReportAction.originalMessage.IOUTransactionID ?? 0; + if (!transactionID) { + return false; + } + // eslint-disable-next-line es/no-nullish-coalescing-operators + const reportID = parentReportAction.originalMessage.IOUReportID ?? 0; + if (!reportID) { + return false; + } + if (!isCurrentUserSubmitter(reportID)) { + return false; + } + return transactionHasViolation(transactionID); +} + +/** + * @param {String} reportID + * @returns {Boolean} + */ + +function reportHasViolations(reportID) { + const transactions = TransactionUtils.getAllReportTransactions(reportID); + return _.some(transactions, (transaction) => transactionHasViolation(transaction.transactionID)); +} + /** * Takes several pieces of data from Onyx and evaluates if a report should be shown in the option list (either when searching * for reports or the reports shown in the LHN). @@ -3369,6 +3440,11 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, return true; } + // Always show IOU reports with violations + if (isExpenseRequest(report) && transactionThreadHasViolations(report)) { + return true; + } + // All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones if (isInGSDMode) { return isUnread(report); @@ -4358,4 +4434,6 @@ export { getReimbursementQueuedActionMessage, getPersonalDetailsForAccountID, getRoom, + transactionThreadHasViolations, + reportHasViolations, }; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 58c4a124335d..2940a6286a32 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -361,7 +361,7 @@ function getOptionData( result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) as OnyxCommon.Errors; - result.brickRoadIndicator = Object.keys(result.allReportErrors ?? {}).length !== 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.brickRoadIndicator = Object.keys(result.allReportErrors ?? {}).length !== 0 || ReportUtils.transactionThreadHasViolations(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; From 3756ef9c53ee0ba0fb0a93d8a4f8a8c5f89750b7 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Thu, 16 Nov 2023 11:03:03 -0500 Subject: [PATCH 0019/1081] Small fix in ReportUtils --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 12808ef747a5..6fe0ff8ad7ea 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3329,7 +3329,7 @@ function transactionThreadHasViolations(report) { return false; } // eslint-disable-next-line es/no-nullish-coalescing-operators - if (!report.parentReportActionID ?? 0) { + if (!report.parentReportActionID) { return false; } From 3cc2e0f6bcc8e9ffbf16127831ea4fc8faebae0d Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Thu, 16 Nov 2023 15:15:20 -0500 Subject: [PATCH 0020/1081] Fix Onyx Connection for reportActions --- src/libs/ReportUtils.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 6fe0ff8ad7ea..e90f2eb955d4 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -95,6 +95,19 @@ Onyx.connect({ }, }); +const reportActions = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + callback: (actions, key) => { + if (!key || !actions) { + return; + } + + const reportID = CollectionUtils.extractCollectionItemID(key); + reportActions[reportID] = actions; + }, +}); + function getChatType(report) { return report ? report.chatType : ''; } @@ -3303,8 +3316,8 @@ function canAccessReport(report, policies, betas, allReportActions) { */ function shouldHideReport(report, currentReportId) { const parentReport = getParentReport(getReport(currentReportId)); - const reportActions = ReportActionsUtils.getAllReportActions(report.reportID); - const isChildReportHasComment = _.some(reportActions, (reportAction) => (reportAction.childVisibleActionCount || 0) > 0); + const allReportActions = ReportActionsUtils.getAllReportActions(report.reportID); + const isChildReportHasComment = _.some(allReportActions, (reportAction) => (reportAction.childVisibleActionCount || 0) > 0); return parentReport.reportID !== report.reportID && !isChildReportHasComment; } @@ -3333,8 +3346,6 @@ function transactionThreadHasViolations(report) { return false; } - const reportActions = ReportActionsUtils.getAllReportActions(report.reportID); - const parentReportAction = lodashGet(reportActions, `${report.parentReportID}.${report.parentReportActionID}`); if (!parentReportAction) { return false; From 3e5c8b094bdec621983d5d075719ba2903bc031f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 20 Nov 2023 17:17:26 +0100 Subject: [PATCH 0021/1081] ref: started migrating LHNOptionsList module --- .../{LHNOptionsList.js => LHNOptionsList.tsx} | 20 ++- .../{OptionRowLHN.js => OptionRowLHN.tsx} | 117 +++++++----------- ...tionRowLHNData.js => OptionRowLHNData.tsx} | 0 3 files changed, 60 insertions(+), 77 deletions(-) rename src/components/LHNOptionsList/{LHNOptionsList.js => LHNOptionsList.tsx} (90%) rename src/components/LHNOptionsList/{OptionRowLHN.js => OptionRowLHN.tsx} (76%) rename src/components/LHNOptionsList/{OptionRowLHNData.js => OptionRowLHNData.tsx} (100%) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.tsx similarity index 90% rename from src/components/LHNOptionsList/LHNOptionsList.js rename to src/components/LHNOptionsList/LHNOptionsList.tsx index 0d300c5e2179..5def414f9010 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -2,8 +2,9 @@ import {FlashList} from '@shopify/flash-list'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {StyleProp, View, ViewStyle} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import _ from 'underscore'; import participantPropTypes from '@components/participantPropTypes'; import transactionPropTypes from '@components/transactionPropTypes'; @@ -17,6 +18,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; import OptionRowLHNData from './OptionRowLHNData'; const propTypes = { @@ -82,6 +84,20 @@ const defaultProps = { const keyExtractor = (item) => `report_${item}`; +type LHNOptionsListProps = { + style?: StyleProp; + contentContainerStyles: StyleProp; + data: string[]; + onSelectRow: (reportID: string) => void; + optionMode: ValueOf; + shouldDisableFocusOptions?: boolean; + policy: OnyxEntry; + reports: OnyxEntry>; + reportActions: OnyxEntry; + preferredLocale: OnyxEntry>; + personalDetails: OnyxEntry>; +}; + function LHNOptionsList({ style, contentContainerStyles, diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.tsx similarity index 76% rename from src/components/LHNOptionsList/OptionRowLHN.js rename to src/components/LHNOptionsList/OptionRowLHN.tsx index 8420f3db7a1e..89133fe27d72 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,8 +1,10 @@ import {useFocusEffect} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useRef, useState} from 'react'; -import {StyleSheet, View} from 'react-native'; +import React, {RefObject, useCallback, useRef, useState} from 'react'; +import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; import _ from 'underscore'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; @@ -30,54 +32,26 @@ import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; +import {Beta} from '@src/types/onyx'; -const propTypes = { - /** Style for hovered state */ - // eslint-disable-next-line react/forbid-prop-types - hoverStyle: PropTypes.object, - - /** List of betas available to current user */ - betas: PropTypes.arrayOf(PropTypes.string), - - /** The ID of the report that the option is for */ - reportID: PropTypes.string.isRequired, - - /** Whether this option is currently in focus so we can modify its style */ - isFocused: PropTypes.bool, - - /** A function that is called when an option is selected. Selected option is passed as a param */ - onSelectRow: PropTypes.func, - - /** Toggle between compact and default view */ - viewMode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)), - - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - /** The item that should be rendered */ - // eslint-disable-next-line react/forbid-prop-types - optionItem: PropTypes.object, -}; - -const defaultProps = { - hoverStyle: undefined, - viewMode: 'default', - onSelectRow: () => {}, - style: null, - optionItem: null, - isFocused: false, - betas: [], +type OptionRowLHNProps = { + hoverStyle?: StyleProp; + betas?: Beta[]; + reportID: string; + isFocused?: boolean; + onSelectRow?: (optionItem: unknown, popoverAnchor: RefObject) => void; + viewMode?: ValueOf; + style?: StyleProp; + optionItem?: unknown; }; - -function OptionRowLHN(props) { +function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style}: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); - const popoverAnchor = useRef(null); + const popoverAnchor = useRef(null); const isFocusedRef = useRef(true); const {isSmallScreenWidth} = useWindowDimensions(); const {translate} = useLocalize(); - - const optionItem = props.optionItem; const [isContextMenuActive, setIsContextMenuActive] = useState(false); useFocusEffect( @@ -94,30 +68,28 @@ function OptionRowLHN(props) { } const isHidden = optionItem.notificationPreference === CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - if (isHidden && !props.isFocused && !optionItem.isPinned) { + if (isHidden && !isFocused && !optionItem.isPinned) { return null; } - const textStyle = props.isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = optionItem.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; - const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], props.style); + const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], style); const alternateTextStyle = StyleUtils.combineStyles( - props.viewMode === CONST.OPTION_MODE.COMPACT + viewMode === CONST.OPTION_MODE.COMPACT ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2] : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting], - props.style, + style, ); const contentContainerStyles = - props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, optionRowStyles.compactContentContainerStyles] : [styles.flex1]; + viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, optionRowStyles.compactContentContainerStyles] : [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten( - props.viewMode === CONST.OPTION_MODE.COMPACT + viewMode === CONST.OPTION_MODE.COMPACT ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); const hoveredBackgroundColor = - (props.hoverStyle || styles.sidebarLinkHover) && (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - ? (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - : theme.sidebar; + (!!hoverStyle || styles.sidebarLinkHover) && (hoverStyle || styles.sidebarLinkHover).backgroundColor ? (hoverStyle || styles.sidebarLinkHover).backgroundColor : theme.sidebar; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; @@ -139,9 +111,9 @@ function OptionRowLHN(props) { event, '', popoverAnchor, - props.reportID, + reportID, '0', - props.reportID, + reportID, '', () => {}, () => setIsContextMenuActive(false), @@ -152,15 +124,14 @@ function OptionRowLHN(props) { ); }; - const emojiCode = lodashGet(optionItem, 'status.emojiCode', ''); - const statusText = lodashGet(optionItem, 'status.text', ''); - const statusClearAfterDate = lodashGet(optionItem, 'status.clearAfter', ''); + const emojiCode = optionItem.status.emojiCode ?? ''; + const statusText = optionItem.status.text ?? ''; + const statusClearAfterDate = optionItem.status.clearAfter ?? ''; const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText} (${formattedDate})` : statusText; - const isStatusVisible = Permissions.canUseCustomStatus(props.betas) && !!emojiCode && ReportUtils.isOneOnOneChat(ReportUtils.getReport(optionItem.reportID)); + const isStatusVisible = Permissions.canUseCustomStatus(betas) && !!emojiCode && ReportUtils.isOneOnOneChat(ReportUtils.getReport(optionItem.reportID)); - const isGroupChat = - optionItem.type === CONST.REPORT.TYPE.CHAT && _.isEmpty(optionItem.chatType) && !optionItem.isThread && lodashGet(optionItem, 'displayNamesWithTooltips.length', 0) > 2; + const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips.length ?? 0) > 2; const fullTitle = isGroupChat ? getGroupChatName(ReportUtils.getReport(optionItem.reportID)) : optionItem.text; return ( @@ -180,7 +151,7 @@ function OptionRowLHN(props) { } // Enable Composer to focus on clicking the same chat after opening the context menu. ReportActionComposeFocusManager.focus(); - props.onSelectRow(optionItem, popoverAnchor); + onSelectRow(optionItem, popoverAnchor); }} onMouseDown={(e) => { // Allow composer blur on right click @@ -196,7 +167,7 @@ function OptionRowLHN(props) { showPopover(e); // Ensure that we blur the composer when opening context menu, so that only one component is focused at a time if (DomUtils.getActiveElement()) { - DomUtils.getActiveElement().blur(); + DomUtils.getActiveElement()?.blur(); } }} withoutFocusOnSecondaryInteraction @@ -208,32 +179,32 @@ function OptionRowLHN(props) { styles.sidebarLink, styles.sidebarLinkInner, StyleUtils.getBackgroundColorStyle(theme.sidebar), - props.isFocused ? styles.sidebarLinkActive : null, - (hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle || styles.sidebarLinkHover : null, + isFocused ? styles.sidebarLinkActive : null, + (hovered || isContextMenuActive) && !isFocused ? hoverStyle ?? styles.sidebarLinkHover : null, ]} role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - needsOffscreenAlphaCompositing={props.optionItem.icons.length >= 2} + needsOffscreenAlphaCompositing={optionItem.icons.length >= 2} > {!_.isEmpty(optionItem.icons) && (optionItem.shouldShowSubscript ? ( ) : ( @@ -321,10 +292,6 @@ function OptionRowLHN(props) { ); } -OptionRowLHN.propTypes = propTypes; -OptionRowLHN.defaultProps = defaultProps; OptionRowLHN.displayName = 'OptionRowLHN'; export default React.memo(OptionRowLHN); - -export {propTypes, defaultProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.tsx similarity index 100% rename from src/components/LHNOptionsList/OptionRowLHNData.js rename to src/components/LHNOptionsList/OptionRowLHNData.tsx From e6d20349facf35a8244f347083741c380d806468 Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Tue, 21 Nov 2023 10:04:19 +0300 Subject: [PATCH 0022/1081] fix wrong cursor style based on accessibility role --- .../Pressable/GenericPressable/BaseGenericPressable.tsx | 2 +- src/components/Pressable/GenericPressable/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index 1576fe18da54..e10f9088d653 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -132,7 +132,7 @@ function GenericPressable( onPressIn={!isDisabled ? onPressIn : undefined} onPressOut={!isDisabled ? onPressOut : undefined} style={(state) => [ - getCursorStyle(shouldUseDisabledCursor, [rest.accessibilityRole, rest.role].includes('text')), + getCursorStyle(shouldUseDisabledCursor, [rest.accessibilityRole, rest.role].includes('presentation')), StyleUtils.parseStyleFromFunction(style, state), isScreenReaderActive && StyleUtils.parseStyleFromFunction(screenReaderActiveStyle, state), state.focused && StyleUtils.parseStyleFromFunction(focusStyle, state), diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx index e0436c26c8fe..523db81863f4 100644 --- a/src/components/Pressable/GenericPressable/index.tsx +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -14,7 +14,7 @@ function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: // change native accessibility props to web accessibility props focusable={focusable} tabIndex={!accessible || !focusable ? -1 : 0} - role={props.accessibilityRole as Role} + role={props.role as Role} id={props.nativeID} aria-label={props.accessibilityLabel} aria-labelledby={props.accessibilityLabelledBy} From f80cf828a546d6d2083bedad905be1f78538964c Mon Sep 17 00:00:00 2001 From: Getabalew Tesfaye Date: Tue, 21 Nov 2023 10:29:12 +0300 Subject: [PATCH 0023/1081] fix lint --- src/components/Pressable/GenericPressable/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx index 523db81863f4..b3b1ff13f5f9 100644 --- a/src/components/Pressable/GenericPressable/index.tsx +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -1,5 +1,5 @@ import React, {ForwardedRef, forwardRef} from 'react'; -import {Role, View} from 'react-native'; +import {View} from 'react-native'; import GenericPressable from './BaseGenericPressable'; import PressableProps from './types'; @@ -14,7 +14,7 @@ function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: // change native accessibility props to web accessibility props focusable={focusable} tabIndex={!accessible || !focusable ? -1 : 0} - role={props.role as Role} + role={props.role} id={props.nativeID} aria-label={props.accessibilityLabel} aria-labelledby={props.accessibilityLabelledBy} From 2418cd131d94790de2b152edfac532ef0d344c9b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 21 Nov 2023 16:16:51 +0100 Subject: [PATCH 0024/1081] ref: working on migration to typescript --- .../LHNOptionsList/LHNOptionsList.tsx | 135 ++++++------------ .../LHNOptionsList/OptionRowLHN.tsx | 12 +- .../LHNOptionsList/OptionRowLHNData.tsx | 103 +++++++------ 3 files changed, 112 insertions(+), 138 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 5def414f9010..160f1330cfa8 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,102 +1,60 @@ -import {FlashList} from '@shopify/flash-list'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import {ContentStyle, FlashList} from '@shopify/flash-list'; import React, {useCallback} from 'react'; import {StyleProp, View, ViewStyle} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; -import _ from 'underscore'; -import participantPropTypes from '@components/participantPropTypes'; -import transactionPropTypes from '@components/transactionPropTypes'; -import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} from '@components/withCurrentReportID'; +import withCurrentReportID, {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; -import reportPropTypes from '@pages/reportPropTypes'; -import stylePropTypes from '@styles/stylePropTypes'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Policy, Report, ReportActions} from '@src/types/onyx'; +import {PersonalDetails, Policy, Report, ReportActions, Transaction} from '@src/types/onyx'; import OptionRowLHNData from './OptionRowLHNData'; -const propTypes = { +const keyExtractor = (item) => `report_${item}`; + +type LHNOptionsListProps = { /** Wrapper style for the section list */ - style: stylePropTypes, + style?: StyleProp; /** Extra styles for the section list container */ - contentContainerStyles: stylePropTypes.isRequired, + contentContainerStyles?: ContentStyle; /** Sections for the section list */ - data: PropTypes.arrayOf(PropTypes.string).isRequired, + data: string[]; /** Callback to fire when a row is selected */ - onSelectRow: PropTypes.func.isRequired, + onSelectRow: (reportID: string) => void; /** Toggle between compact and default view of the option */ - optionMode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)).isRequired, + optionMode: ValueOf; /** Whether to allow option focus or not */ - shouldDisableFocusOptions: PropTypes.bool, + shouldDisableFocusOptions?: boolean; /** The policy which the user has access to and which the report could be tied to */ - policy: PropTypes.shape({ - /** The ID of the policy */ - id: PropTypes.string, - /** Name of the policy */ - name: PropTypes.string, - /** Avatar of the policy */ - avatar: PropTypes.string, - }), + policy: OnyxEntry>; /** All reports shared with the user */ - reports: PropTypes.objectOf(reportPropTypes), + reports: OnyxEntry>; /** Array of report actions for this report */ - reportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), + reportActions: OnyxEntry>; /** Indicates which locale the user currently has selected */ - preferredLocale: PropTypes.string, + preferredLocale: OnyxEntry>; /** List of users' personal details */ - personalDetails: PropTypes.objectOf(participantPropTypes), + personalDetails: OnyxEntry>; /** The transaction from the parent report action */ - transactions: PropTypes.objectOf(transactionPropTypes), - /** List of draft comments */ - draftComments: PropTypes.objectOf(PropTypes.string), - ...withCurrentReportIDPropTypes, -}; - -const defaultProps = { - style: undefined, - shouldDisableFocusOptions: false, - reportActions: {}, - reports: {}, - policy: {}, - preferredLocale: CONST.LOCALES.DEFAULT, - personalDetails: {}, - transactions: {}, - draftComments: {}, - ...withCurrentReportIDDefaultProps, -}; + transactions: OnyxEntry>; -const keyExtractor = (item) => `report_${item}`; - -type LHNOptionsListProps = { - style?: StyleProp; - contentContainerStyles: StyleProp; - data: string[]; - onSelectRow: (reportID: string) => void; - optionMode: ValueOf; - shouldDisableFocusOptions?: boolean; - policy: OnyxEntry; - reports: OnyxEntry>; - reportActions: OnyxEntry; - preferredLocale: OnyxEntry>; - personalDetails: OnyxEntry>; -}; + /** List of draft comments */ + draftComments: OnyxEntry>; +} & CurrentReportIDContextValue; function LHNOptionsList({ style, @@ -104,36 +62,31 @@ function LHNOptionsList({ data, onSelectRow, optionMode, - shouldDisableFocusOptions, - reports, - reportActions, - policy, - preferredLocale, - personalDetails, - transactions, - draftComments, - currentReportID, -}) { + shouldDisableFocusOptions = false, + reports = {}, + reportActions = {}, + policy = {}, + preferredLocale = CONST.LOCALES.DEFAULT, + personalDetails = {}, + transactions = {}, + draftComments = {}, + currentReportID = '', +}: LHNOptionsListProps) { const styles = useThemeStyles(); /** * Function which renders a row in the list - * - * @param {Object} params - * @param {Object} params.item - * - * @return {Component} */ const renderItem = useCallback( - ({item: reportID}) => { - const itemFullReport = reports[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] || {}; - const itemReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - const itemParentReportActions = reportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport.parentReportID}`] || {}; - const itemParentReportAction = itemParentReportActions[itemFullReport.parentReportActionID] || {}; - const itemPolicy = policy[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport.policyID}`] || {}; - const transactionID = lodashGet(itemParentReportAction, ['originalMessage', 'IOUTransactionID'], ''); - const itemTransaction = transactionID ? transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {}; - const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || ''; - const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport.participantAccountIDs, personalDetails); + ({item: reportID}: {item: string}) => { + const itemFullReport: Report | undefined = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; + const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; + const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`]; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? '']; + const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`]; + const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID : ''; + const itemTransaction = transactionID ? transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {}; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; + const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport?.participantAccountIDs ?? [], personalDetails); return ( + ; /** The preferred language for the app */ - preferredLocale: PropTypes.string, + preferredLocale: string; /** The full data of the report */ - // eslint-disable-next-line react/forbid-prop-types - fullReport: PropTypes.object, + fullReport: Report; /** The policy which the user has access to and which the report could be tied to */ - policy: PropTypes.shape({ - /** The ID of the policy */ - id: PropTypes.string, - /** Name of the policy */ - name: PropTypes.string, - /** Avatar of the policy */ - avatar: PropTypes.string, - }), + policy: Policy; /** The action from the parent report */ - parentReportAction: PropTypes.shape(reportActionPropTypes), + parentReportAction: ReportAction; /** The transaction from the parent report action */ - transaction: transactionPropTypes, - - ...basePropTypes, -}; - -const defaultProps = { - isFocused: false, - personalDetails: {}, - fullReport: {}, - policy: {}, - parentReportAction: {}, - transaction: {}, - preferredLocale: CONST.LOCALES.DEFAULT, - ...baseDefaultProps, -}; + transaction: Transaction; + + comment: string; + + receiptTransactions: Transaction[]; +} & LHNOptionsListProps; /* * This component gets the data from onyx for the actual @@ -74,7 +97,7 @@ function OptionRowLHNData({ parentReportAction, transaction, ...propsToForward -}) { +}: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; const optionItemRef = useRef(); @@ -116,8 +139,6 @@ function OptionRowLHNData({ ); } -OptionRowLHNData.propTypes = propTypes; -OptionRowLHNData.defaultProps = defaultProps; OptionRowLHNData.displayName = 'OptionRowLHNData'; /** From a98b3a12290754acc7610c3e84083a9ac9f515a6 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Tue, 21 Nov 2023 12:17:38 -0500 Subject: [PATCH 0025/1081] Fix Permissions import --- src/libs/ReportUtils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index cc7e841de251..78f636a7ad74 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -8,6 +8,8 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; +// eslint-disable-next-line @dword-design/import-alias/prefer-alias +import Permissions from '@libs/Permissions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -20,7 +22,6 @@ import * as Localize from './Localize'; import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; -import Permissions from './Permissions'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as TransactionUtils from './TransactionUtils'; From 2e6f52a91aa81c4c8ab11703f8f20fc29770f0e9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 21 Nov 2023 20:21:41 +0100 Subject: [PATCH 0026/1081] ref: contuniue migration --- .../LHNOptionsList/LHNOptionsList.tsx | 44 +-------- .../LHNOptionsList/OptionRowLHN.tsx | 29 ++---- .../LHNOptionsList/OptionRowLHNData.tsx | 46 ++------- src/components/LHNOptionsList/types.ts | 95 +++++++++++++++++++ src/components/SubscriptAvatar.tsx | 32 +++---- src/libs/SidebarUtils.ts | 2 + 6 files changed, 127 insertions(+), 121 deletions(-) create mode 100644 src/components/LHNOptionsList/types.ts diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 160f1330cfa8..64acf390e6a6 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -12,50 +12,10 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {PersonalDetails, Policy, Report, ReportActions, Transaction} from '@src/types/onyx'; import OptionRowLHNData from './OptionRowLHNData'; +import {LHNOptionsListProps} from './types'; const keyExtractor = (item) => `report_${item}`; -type LHNOptionsListProps = { - /** Wrapper style for the section list */ - style?: StyleProp; - - /** Extra styles for the section list container */ - contentContainerStyles?: ContentStyle; - - /** Sections for the section list */ - data: string[]; - - /** Callback to fire when a row is selected */ - onSelectRow: (reportID: string) => void; - - /** Toggle between compact and default view of the option */ - optionMode: ValueOf; - - /** Whether to allow option focus or not */ - shouldDisableFocusOptions?: boolean; - - /** The policy which the user has access to and which the report could be tied to */ - policy: OnyxEntry>; - - /** All reports shared with the user */ - reports: OnyxEntry>; - - /** Array of report actions for this report */ - reportActions: OnyxEntry>; - - /** Indicates which locale the user currently has selected */ - preferredLocale: OnyxEntry>; - - /** List of users' personal details */ - personalDetails: OnyxEntry>; - - /** The transaction from the parent report action */ - transactions: OnyxEntry>; - - /** List of draft comments */ - draftComments: OnyxEntry>; -} & CurrentReportIDContextValue; - function LHNOptionsList({ style, contentContainerStyles, @@ -69,8 +29,8 @@ function LHNOptionsList({ preferredLocale = CONST.LOCALES.DEFAULT, personalDetails = {}, transactions = {}, - draftComments = {}, currentReportID = '', + draftComments = {}, }: LHNOptionsListProps) { const styles = useThemeStyles(); /** diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 72add9bc8ae5..24d006ae7555 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,11 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {RefObject, useCallback, useRef, useState} from 'react'; -import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; -import {OnyxEntry} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; -import _ from 'underscore'; +import React, {useCallback, useRef, useState} from 'react'; +import {StyleSheet, View} from 'react-native'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -32,18 +27,8 @@ import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import {Beta} from '@src/types/onyx'; +import {OptionRowLHNProps} from './types'; -type OptionRowLHNProps = { - hoverStyle?: StyleProp; - betas?: Beta[]; - reportID: string; - isFocused?: boolean; - onSelectRow?: (optionItem: unknown, popoverAnchor: RefObject) => void; - viewMode?: ValueOf; - style?: StyleProp; - optionItem?: unknown; -}; function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style}: OptionRowLHNProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -184,7 +169,7 @@ function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSe ]} role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.navigatesToChat')} - needsOffscreenAlphaCompositing={optionItem.icons.length >= 2} + needsOffscreenAlphaCompositing={(optionItem?.icons?.length ?? 0) >= 2} > @@ -192,13 +177,13 @@ function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSe (optionItem.shouldShowSubscript ? ( ) : ( ; - - /** The preferred language for the app */ - preferredLocale: string; - - /** The full data of the report */ - fullReport: Report; - - /** The policy which the user has access to and which the report could be tied to */ - policy: Policy; - - /** The action from the parent report */ - parentReportAction: ReportAction; - - /** The transaction from the parent report action */ - transaction: Transaction; - - comment: string; - - receiptTransactions: Transaction[]; -} & LHNOptionsListProps; - /* * This component gets the data from onyx for the actual * OptionRowLHN component. @@ -100,10 +72,10 @@ function OptionRowLHNData({ }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; - const optionItemRef = useRef(); + const optionItemRef = useRef(); const linkedTransaction = useMemo(() => { const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(reportActions); - const lastReportAction = _.first(sortedReportActions); + const lastReportAction = sortedReportActions[0]; return TransactionUtils.getLinkedTransaction(lastReportAction); // eslint-disable-next-line react-hooks/exhaustive-deps }, [fullReport.reportID, receiptTransactions, reportActions]); @@ -114,7 +86,9 @@ function OptionRowLHNData({ if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } - optionItemRef.current = item; + if (item) { + optionItemRef.current = item; + } return item; // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed @@ -122,10 +96,10 @@ function OptionRowLHNData({ }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); useEffect(() => { - if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { + if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { return; } - Report.setReportWithDraft(reportID, true); + ReportLib.setReportWithDraft(reportID, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts new file mode 100644 index 000000000000..2f7bfc1005c0 --- /dev/null +++ b/src/components/LHNOptionsList/types.ts @@ -0,0 +1,95 @@ +import {ContentStyle} from '@shopify/flash-list'; +import {Transaction} from 'electron'; +import {RefObject} from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; +import {OnyxEntry} from 'react-native-onyx'; +import {ValueOf} from 'type-fest'; +import {CurrentReportIDContextValue} from '@components/withCurrentReportID'; +import {OptionData} from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import {Beta, PersonalDetails, Policy, Report, ReportAction, ReportActions} from '@src/types/onyx'; + +type CustomLHNOptionsListProps = { + /** Wrapper style for the section list */ + style?: StyleProp; + + /** Extra styles for the section list container */ + contentContainerStyles?: ContentStyle; + + /** Sections for the section list */ + data: string[]; + + /** Callback to fire when a row is selected */ + onSelectRow: (reportID: string) => void; + + /** Toggle between compact and default view of the option */ + optionMode: ValueOf; + + /** Whether to allow option focus or not */ + shouldDisableFocusOptions?: boolean; + + /** The policy which the user has access to and which the report could be tied to */ + policy: OnyxEntry>; + + /** All reports shared with the user */ + reports: OnyxEntry>; + + /** Array of report actions for this report */ + reportActions: OnyxEntry>; + + /** Indicates which locale the user currently has selected */ + preferredLocale: OnyxEntry>; + + /** List of users' personal details */ + personalDetails: OnyxEntry>; + + /** The transaction from the parent report action */ + transactions: OnyxEntry>; + + /** List of draft comments */ + draftComments: OnyxEntry>; +}; + +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue; + +type OptionRowLHNDataProps = { + /** Whether row should be focused */ + isFocused: boolean; + + /** List of users' personal details */ + personalDetails: Record; + + /** The preferred language for the app */ + preferredLocale: string; + + /** The full data of the report */ + fullReport: Report; + + /** The policy which the user has access to and which the report could be tied to */ + policy: Policy; + + /** The action from the parent report */ + parentReportAction: ReportAction; + + /** The transaction from the parent report action */ + transaction: Transaction; + + comment: string; + + receiptTransactions: Transaction[]; + + reportID: string; +} & CustomLHNOptionsListProps; + +type OptionRowLHNProps = { + hoverStyle?: StyleProp; + betas?: Beta[]; + reportID: string; + isFocused?: boolean; + onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; + viewMode?: ValueOf; + style?: StyleProp; + optionItem?: OptionData; +}; + +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps}; diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index ab9f0dec8e57..8b77565e25ff 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -1,37 +1,20 @@ import React, {memo} from 'react'; import {View} from 'react-native'; import {ValueOf} from 'type-fest'; -import type {AvatarSource} from '@libs/UserUtils'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; +import {Icon} from '@src/types/onyx/OnyxCommon'; import Avatar from './Avatar'; import UserDetailsTooltip from './UserDetailsTooltip'; -type SubAvatar = { - /** Avatar source to display */ - source?: AvatarSource; - - /** Denotes whether it is an avatar or a workspace avatar */ - type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE; - - /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name?: string; - - /** Avatar id */ - id?: number | string; - - /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon?: AvatarSource; -}; - type SubscriptAvatarProps = { /** Avatar URL or icon */ - mainAvatar?: SubAvatar; + mainAvatar?: Icon; /** Subscript avatar URL or icon */ - secondaryAvatar?: SubAvatar; + secondaryAvatar?: Icon; /** Set the size of avatars */ size?: ValueOf; @@ -46,7 +29,14 @@ type SubscriptAvatarProps = { showTooltip?: boolean; }; -function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AVATAR_SIZE.DEFAULT, backgroundColor, noMargin = false, showTooltip = true}: SubscriptAvatarProps) { +function SubscriptAvatar({ + mainAvatar = {} as Icon, + secondaryAvatar = {} as Icon, + size = CONST.AVATAR_SIZE.DEFAULT, + backgroundColor, + noMargin = false, + showTooltip = true, +}: SubscriptAvatarProps) { const theme = useTheme(); const styles = useThemeStyles(); const isSmall = size === CONST.AVATAR_SIZE.SMALL; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 58c4a124335d..8aa01543ddae 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -527,3 +527,5 @@ export default { isSidebarLoadedReady, resetIsSidebarLoadedReadyPromise, }; + +export type {OptionData}; From 6c72cd965b0620907d4a75e8fe42ae874ce069d5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 22 Nov 2023 13:09:20 +0100 Subject: [PATCH 0027/1081] fix: add few type fixes --- .../LHNOptionsList/LHNOptionsList.tsx | 63 ++++++++++++------- .../LHNOptionsList/OptionRowLHNData.tsx | 50 +++------------ src/components/LHNOptionsList/types.ts | 62 +++++++++--------- src/libs/SidebarUtils.ts | 22 ++++--- 4 files changed, 96 insertions(+), 101 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 64acf390e6a6..967525b0ff6a 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,20 +1,17 @@ -import {ContentStyle, FlashList} from '@shopify/flash-list'; import React, {useCallback} from 'react'; -import {StyleProp, View, ViewStyle} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {ValueOf} from 'type-fest'; -import withCurrentReportID, {CurrentReportIDContextValue} from '@components/withCurrentReportID'; +import {FlatList, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import withCurrentReportID from '@components/withCurrentReportID'; import compose from '@libs/compose'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {PersonalDetails, Policy, Report, ReportActions, Transaction} from '@src/types/onyx'; import OptionRowLHNData from './OptionRowLHNData'; -import {LHNOptionsListProps} from './types'; +import {LHNOptionsListOnyxProps, LHNOptionsListProps} from './types'; -const keyExtractor = (item) => `report_${item}`; +const keyExtractor = (item: string) => `report_${item}`; function LHNOptionsList({ style, @@ -33,21 +30,41 @@ function LHNOptionsList({ draftComments = {}, }: LHNOptionsListProps) { const styles = useThemeStyles(); + + /** + * This function is used to compute the layout of any given item in our list. Since we know that each item will have the exact same height, this is a performance optimization + * so that the heights can be determined before the options are rendered. Otherwise, the heights are determined when each option is rendering and it causes a lot of overhead on large + * lists. + * + * @param itemData - This is the same as the data we pass into the component + * @param index the current item's index in the set of data + */ + const getItemLayout = useCallback( + // eslint-disable-next-line @typescript-eslint/naming-convention + (_, index: number) => { + const optionHeight = optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight; + return { + length: optionHeight, + offset: index * optionHeight, + index, + }; + }, + [optionMode], + ); /** * Function which renders a row in the list */ const renderItem = useCallback( ({item: reportID}: {item: string}) => { - const itemFullReport: Report | undefined = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]; - const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`]; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? '']; - const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`]; - const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID : ''; - const itemTransaction = transactionID ? transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {}; + const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; + const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; + const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport?.participantAccountIDs ?? [], personalDetails); - return ( - ); @@ -90,8 +109,7 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; export default compose( - withCurrentReportID, - withOnyx({ + withOnyx({ reports: { key: ONYXKEYS.COLLECTION.REPORT, }, @@ -114,6 +132,7 @@ export default compose( key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, }), + withCurrentReportID, )(LHNOptionsList); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index de2e3742a817..4abc69828791 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -4,42 +4,10 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import SidebarUtils, {OptionData} from '@libs/SidebarUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as ReportLib from '@userActions/Report'; +import CONST from '@src/CONST'; import OptionRowLHN from './OptionRowLHN'; import {OptionRowLHNDataProps} from './types'; -// const propTypes = { -// /** Whether row should be focused */ -// isFocused: PropTypes.bool, - -// /** List of users' personal details */ -// personalDetails: PropTypes.objectOf(participantPropTypes), - -// /** The preferred language for the app */ -// preferredLocale: PropTypes.string, - -// /** The full data of the report */ -// // eslint-disable-next-line react/forbid-prop-types -// fullReport: PropTypes.object, - -// /** The policy which the user has access to and which the report could be tied to */ -// policy: PropTypes.shape({ -// /** The ID of the policy */ -// id: PropTypes.string, -// /** Name of the policy */ -// name: PropTypes.string, -// /** Avatar of the policy */ -// avatar: PropTypes.string, -// }), - -// /** The action from the parent report */ -// parentReportAction: PropTypes.shape(reportActionPropTypes), - -// /** The transaction from the parent report action */ -// transaction: transactionPropTypes, - -// ...basePropTypes, -// }; - // const defaultProps = { // isFocused: false, // personalDetails: {}, @@ -58,16 +26,16 @@ import {OptionRowLHNDataProps} from './types'; * re-render if the data really changed. */ function OptionRowLHNData({ - isFocused, - fullReport, + isFocused = false, + fullReport = null, reportActions, - personalDetails, - preferredLocale, + personalDetails = {}, + preferredLocale = CONST.LOCALES.DEFAULT, comment, - policy, + policy = null, receiptTransactions, - parentReportAction, - transaction, + parentReportAction = null, + transaction = null, ...propsToForward }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; @@ -78,7 +46,7 @@ function OptionRowLHNData({ const lastReportAction = sortedReportActions[0]; return TransactionUtils.getLinkedTransaction(lastReportAction); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport.reportID, receiptTransactions, reportActions]); + }, [fullReport?.reportID, receiptTransactions, reportActions]); const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 2f7bfc1005c0..5a736e57721d 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -1,5 +1,4 @@ import {ContentStyle} from '@shopify/flash-list'; -import {Transaction} from 'electron'; import {RefObject} from 'react'; import {StyleProp, ViewStyle} from 'react-native'; import {OnyxEntry} from 'react-native-onyx'; @@ -7,27 +6,9 @@ import {ValueOf} from 'type-fest'; import {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import {OptionData} from '@libs/SidebarUtils'; import CONST from '@src/CONST'; -import {Beta, PersonalDetails, Policy, Report, ReportAction, ReportActions} from '@src/types/onyx'; - -type CustomLHNOptionsListProps = { - /** Wrapper style for the section list */ - style?: StyleProp; - - /** Extra styles for the section list container */ - contentContainerStyles?: ContentStyle; - - /** Sections for the section list */ - data: string[]; - - /** Callback to fire when a row is selected */ - onSelectRow: (reportID: string) => void; - - /** Toggle between compact and default view of the option */ - optionMode: ValueOf; - - /** Whether to allow option focus or not */ - shouldDisableFocusOptions?: boolean; +import {Beta, PersonalDetails, Policy, Report, ReportAction, ReportActions, Transaction} from '@src/types/onyx'; +type LHNOptionsListOnyxProps = { /** The policy which the user has access to and which the report could be tied to */ policy: OnyxEntry>; @@ -49,8 +30,27 @@ type CustomLHNOptionsListProps = { /** List of draft comments */ draftComments: OnyxEntry>; }; +type CustomLHNOptionsListProps = { + /** Wrapper style for the section list */ + style?: StyleProp; + + /** Extra styles for the section list container */ + contentContainerStyles?: ContentStyle; + + /** Sections for the section list */ + data: string[]; + + /** Callback to fire when a row is selected */ + onSelectRow: (reportID: string) => void; -type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue; + /** Toggle between compact and default view of the option */ + optionMode: ValueOf; + + /** Whether to allow option focus or not */ + shouldDisableFocusOptions?: boolean; +}; + +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; type OptionRowLHNDataProps = { /** Whether row should be focused */ @@ -60,26 +60,28 @@ type OptionRowLHNDataProps = { personalDetails: Record; /** The preferred language for the app */ - preferredLocale: string; + preferredLocale: OnyxEntry>; /** The full data of the report */ - fullReport: Report; + fullReport: OnyxEntry; /** The policy which the user has access to and which the report could be tied to */ - policy: Policy; + policy: OnyxEntry; /** The action from the parent report */ - parentReportAction: ReportAction; + parentReportAction: OnyxEntry; /** The transaction from the parent report action */ - transaction: Transaction; + transaction: OnyxEntry; comment: string; - receiptTransactions: Transaction[]; + receiptTransactions: OnyxEntry>; reportID: string; -} & CustomLHNOptionsListProps; + + reportActions: OnyxEntry; +}; type OptionRowLHNProps = { hoverStyle?: StyleProp; @@ -92,4 +94,4 @@ type OptionRowLHNProps = { optionItem?: OptionData; }; -export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps}; +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps}; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index d3bc3e609486..824e397376ab 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,6 +1,6 @@ /* eslint-disable rulesdir/prefer-underscore-method */ import Str from 'expensify-common/lib/str'; -import Onyx from 'react-native-onyx'; +import Onyx, {OnyxEntry} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -219,6 +219,12 @@ function getOrderedReportIDs( return LHNReports; } +type Status = { + text: string; + emojiCode: string; + clearAfter: string; +}; + type OptionData = { text?: string | null; alternateText?: string | null; @@ -235,7 +241,7 @@ type OptionData = { managerID?: number | null; reportID?: string | null; policyID?: string | null; - status?: string | null; + status?: Status | null; type?: string | null; stateNum?: ValueOf | null; statusNum?: ValueOf | null; @@ -292,12 +298,12 @@ type Icon = { * Gets all the data necessary for rendering an OptionRowLHN component */ function getOptionData( - report: Report, - reportActions: Record, - personalDetails: Record, - preferredLocale: ValueOf, - policy: Policy, - parentReportAction: ReportAction, + report: OnyxEntry, + reportActions: OnyxEntry>, + personalDetails: OnyxEntry>, + preferredLocale: OnyxEntry>, + policy: OnyxEntry, + parentReportAction: OnyxEntry, ): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do From f7dc7311cba335f50fc44fa36616a9e082f121c1 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Wed, 22 Nov 2023 09:50:25 -0500 Subject: [PATCH 0028/1081] Correctly pass down betas --- src/components/LHNOptionsList/LHNOptionsList.js | 11 ++++++++++- src/components/LHNOptionsList/OptionRowLHNData.js | 2 +- src/libs/ReportUtils.js | 10 +++++----- src/libs/SidebarUtils.ts | 4 +++- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 0d300c5e2179..605ad761240a 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -64,6 +64,9 @@ const propTypes = { transactions: PropTypes.objectOf(transactionPropTypes), /** List of draft comments */ draftComments: PropTypes.objectOf(PropTypes.string), + + /** The list of betas the user has access to */ + betas: PropTypes.arrayOf(PropTypes.string), ...withCurrentReportIDPropTypes, }; @@ -77,6 +80,7 @@ const defaultProps = { personalDetails: {}, transactions: {}, draftComments: {}, + betas: {}, ...withCurrentReportIDDefaultProps, }; @@ -97,6 +101,7 @@ function LHNOptionsList({ transactions, draftComments, currentReportID, + betas, }) { const styles = useThemeStyles(); /** @@ -134,10 +139,11 @@ function LHNOptionsList({ onSelectRow={onSelectRow} preferredLocale={preferredLocale} comment={itemComment} + betas={betas} /> ); }, - [currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], + [currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions, betas], ); return ( @@ -186,5 +192,8 @@ export default compose( draftComments: { key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, }, + betas: { + key: ONYXKEYS.BETAS, + }, }), )(LHNOptionsList); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index e11bfc3cab98..f3b0e78b0df0 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -87,7 +87,7 @@ function OptionRowLHNData({ const optionItem = useMemo(() => { // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction); + const item = SidebarUtils.getOptionData(fullReport, reportActions, personalDetails, preferredLocale, policy, parentReportAction, propsToForward.betas); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 78f636a7ad74..bac1b45440c3 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -8,8 +8,6 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; -// eslint-disable-next-line @dword-design/import-alias/prefer-alias -import Permissions from '@libs/Permissions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -22,6 +20,7 @@ import * as Localize from './Localize'; import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; +import Permissions from './Permissions'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import * as TransactionUtils from './TransactionUtils'; @@ -3361,14 +3360,15 @@ function transactionHasViolation(transactionID) { /** * * @param {Object} report + * @param {Array | null} betas * @returns {Boolean} */ -function transactionThreadHasViolations(report) { - if (!Permissions.canUseViolations()) { +function transactionThreadHasViolations(report, betas) { + if (!Permissions.canUseViolations(betas)) { return false; } - // eslint-disable-next-line es/no-nullish-coalescing-operators + if (!report.parentReportActionID) { return false; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2940a6286a32..510ca2347a14 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -299,6 +299,7 @@ function getOptionData( preferredLocale: ValueOf, policy: Policy, parentReportAction: ReportAction, + betas: Beta[], ): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do @@ -361,7 +362,8 @@ function getOptionData( result.shouldShowSubscript = ReportUtils.shouldReportShowSubscript(report); result.pendingAction = report.pendingFields ? report.pendingFields.addWorkspaceRoom || report.pendingFields.createChat : null; result.allReportErrors = OptionsListUtils.getAllReportErrors(report, reportActions) as OnyxCommon.Errors; - result.brickRoadIndicator = Object.keys(result.allReportErrors ?? {}).length !== 0 || ReportUtils.transactionThreadHasViolations(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + result.brickRoadIndicator = + Object.keys(result.allReportErrors ?? {}).length !== 0 || ReportUtils.transactionThreadHasViolations(report, betas) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; result.ownerAccountID = report.ownerAccountID; result.managerID = report.managerID; result.reportID = report.reportID; From 2bf40c8f31db5342889c0b2845ec63f87c7a95ec Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 22 Nov 2023 19:37:24 +0100 Subject: [PATCH 0029/1081] fix: types --- src/components/DisplayNames/types.ts | 2 +- .../LHNOptionsList/OptionRowLHN.tsx | 43 +++++++++++-------- .../LHNOptionsList/OptionRowLHNData.tsx | 11 ----- src/components/LHNOptionsList/types.ts | 2 +- src/libs/SidebarUtils.ts | 12 ++---- 5 files changed, 29 insertions(+), 41 deletions(-) diff --git a/src/components/DisplayNames/types.ts b/src/components/DisplayNames/types.ts index 94e4fc7c39c6..307c28bd2df3 100644 --- a/src/components/DisplayNames/types.ts +++ b/src/components/DisplayNames/types.ts @@ -12,7 +12,7 @@ type DisplayNameWithTooltip = { login?: string; /** The avatar for the tooltip fallback */ - avatar: AvatarSource; + avatar?: AvatarSource; }; type DisplayNamesProps = { diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 24d006ae7555..e2db0cba498a 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -1,6 +1,6 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback, useRef, useState} from 'react'; -import {StyleSheet, View} from 'react-native'; +import {StyleProp, StyleSheet, TextStyle, View} from 'react-native'; import DisplayNames from '@components/DisplayNames'; import Hoverable from '@components/Hoverable'; import Icon from '@components/Icon'; @@ -59,22 +59,24 @@ function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSe const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = optionItem?.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; - const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], style); + const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], style ?? {}); const alternateTextStyle = StyleUtils.combineStyles( viewMode === CONST.OPTION_MODE.COMPACT ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2] : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting], - style, + style ?? {}, ); const contentContainerStyles = viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, optionRowStyles.compactContentContainerStyles] : [styles.flex1]; - const sidebarInnerRowStyle = StyleSheet.flatten( - viewMode === CONST.OPTION_MODE.COMPACT - ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] - : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], - ); + const sidebarInnerRowStyle = StyleSheet.flatten([ + styles.chatLinkRowPressable, + styles.flexGrow1, + styles.optionItemAvatarNameWrapper, + viewMode === CONST.OPTION_MODE.COMPACT ? styles.optionRowCompact : styles.optionRow, + styles.justifyContentCenter, + ]); const hoveredBackgroundColor = - (!!hoverStyle || styles.sidebarLinkHover) && (hoverStyle || styles.sidebarLinkHover).backgroundColor ? (hoverStyle || styles.sidebarLinkHover).backgroundColor : theme.sidebar; + !!hoverStyle && 'backgroundColor' in hoverStyle && 'backgroundColor' in styles.sidebarLinkHover ? (hoverStyle ?? styles.sidebarLinkHover).backgroundColor : theme.sidebar; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; const hasBrickError = optionItem.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; @@ -84,7 +86,7 @@ function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSe /** * Show the ReportActionContextMenu modal popover. * - * @param {Object} [event] - A press event. + * @param [event] - A press event. */ const showPopover = (event) => { if (!isFocusedRef.current && isSmallScreenWidth) { @@ -114,11 +116,10 @@ function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSe const statusClearAfterDate = optionItem.status?.clearAfter ?? ''; const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText} (${formattedDate})` : statusText; - const isStatusVisible = Permissions.canUseCustomStatus(betas) && !!emojiCode && ReportUtils.isOneOnOneChat(ReportUtils.getReport(optionItem.reportID)); - - const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips.length ?? 0) > 2; - const fullTitle = isGroupChat ? getGroupChatName(ReportUtils.getReport(optionItem.reportID)) : optionItem.text; + const isStatusVisible = Permissions.canUseCustomStatus(betas) && !!emojiCode && ReportUtils.isOneOnOneChat(ReportUtils.getReport(optionItem?.reportID ?? '')); + const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (optionItem?.displayNamesWithTooltips?.length ?? 0) > 2; + const fullTitle = isGroupChat ? getGroupChatName(ReportUtils.getReport(optionItem?.reportID ?? '')) : optionItem.text; return ( {isStatusVisible && ( @@ -226,9 +231,9 @@ function OptionRowLHN({hoverStyle, betas = [], reportID, isFocused = false, onSe ) : null} - {optionItem.descriptiveText ? ( + {optionItem?.descriptiveText ? ( - {optionItem.descriptiveText} + {optionItem?.descriptiveText} ) : null} {hasBrickError && ( diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 4abc69828791..b48454fb7ec9 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -8,17 +8,6 @@ import CONST from '@src/CONST'; import OptionRowLHN from './OptionRowLHN'; import {OptionRowLHNDataProps} from './types'; -// const defaultProps = { -// isFocused: false, -// personalDetails: {}, -// fullReport: {}, -// policy: {}, -// parentReportAction: {}, -// transaction: {}, -// preferredLocale: CONST.LOCALES.DEFAULT, -// ...baseDefaultProps, -// }; - /* * This component gets the data from onyx for the actual * OptionRowLHN component. diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 5a736e57721d..fa0e48a413c4 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -90,7 +90,7 @@ type OptionRowLHNProps = { isFocused?: boolean; onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; viewMode?: ValueOf; - style?: StyleProp; + style?: ViewStyle | ViewStyle[]; optionItem?: OptionData; }; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 824e397376ab..5432662dcc94 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -231,7 +231,7 @@ type OptionData = { pendingAction?: OnyxCommon.PendingAction | null; allReportErrors?: OnyxCommon.Errors | null; brickRoadIndicator?: typeof CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR | '' | null; - icons?: Icon[] | null; + icons?: OnyxCommon.Icon[] | null; tooltipText?: string | null; ownerAccountID?: number | null; subtitle?: string | null; @@ -272,11 +272,12 @@ type OptionData = { notificationPreference?: string | number | null; displayNamesWithTooltips?: DisplayNamesWithTooltip[] | null; chatType?: ValueOf | null; + descriptiveText?: string; }; type DisplayNamesWithTooltip = { displayName?: string; - avatar?: string; + avatar?: UserUtils.AvatarSource; login?: string; accountID?: number; pronouns?: string; @@ -287,13 +288,6 @@ type ActorDetails = { accountID?: number; }; -type Icon = { - source?: string; - id?: number; - type?: string; - name?: string; -}; - /** * Gets all the data necessary for rendering an OptionRowLHN component */ From 93ea5b7de9f4b9ef3409e1250549590ae1eabc29 Mon Sep 17 00:00:00 2001 From: Daniel Edwards Date: Wed, 22 Nov 2023 13:38:37 -0500 Subject: [PATCH 0030/1081] Update tests --- src/libs/__mocks__/Permissions.ts | 1 + tests/unit/SidebarFilterTest.js | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/libs/__mocks__/Permissions.ts b/src/libs/__mocks__/Permissions.ts index e95d13f52803..23939d037f9a 100644 --- a/src/libs/__mocks__/Permissions.ts +++ b/src/libs/__mocks__/Permissions.ts @@ -13,4 +13,5 @@ export default { canUseDefaultRooms: (betas: Beta[]) => betas.includes(CONST.BETAS.DEFAULT_ROOMS), canUsePolicyRooms: (betas: Beta[]) => betas.includes(CONST.BETAS.POLICY_ROOMS), canUseCustomStatus: (betas: Beta[]) => betas.includes(CONST.BETAS.CUSTOM_STATUS), + canUseViolations: (betas: Beta[]) => betas.includes(CONST.BETAS.VIOLATIONS), }; diff --git a/tests/unit/SidebarFilterTest.js b/tests/unit/SidebarFilterTest.js index 23a958e3aa9d..f4a4a64a3907 100644 --- a/tests/unit/SidebarFilterTest.js +++ b/tests/unit/SidebarFilterTest.js @@ -111,6 +111,7 @@ describe('Sidebar', () => { // When Onyx is updated to contain that report .then(() => Onyx.multiSet({ + [ONYXKEYS.BETAS]: [CONST.BETAS.VIOLATIONS], [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, @@ -141,7 +142,7 @@ describe('Sidebar', () => { // When Onyx is updated to contain that data and the sidebar re-renders .then(() => Onyx.multiSet({ - [ONYXKEYS.BETAS]: [], + [ONYXKEYS.BETAS]: [CONST.BETAS.VIOLATIONS], [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, @@ -194,7 +195,7 @@ describe('Sidebar', () => { // When Onyx is updated to contain that data and the sidebar re-renders .then(() => Onyx.multiSet({ - [ONYXKEYS.BETAS]: [], + [ONYXKEYS.BETAS]: [CONST.BETAS.VIOLATIONS], [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, @@ -246,7 +247,7 @@ describe('Sidebar', () => { // When Onyx is updated to contain that data and the sidebar re-renders .then(() => Onyx.multiSet({ - [ONYXKEYS.BETAS]: [], + [ONYXKEYS.BETAS]: [CONST.BETAS.VIOLATIONS], [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, @@ -379,6 +380,7 @@ describe('Sidebar', () => { // When Onyx is updated to contain that data and the sidebar re-renders .then(() => Onyx.multiSet({ + [ONYXKEYS.BETAS]: [CONST.BETAS.VIOLATIONS], [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, @@ -445,11 +447,14 @@ describe('Sidebar', () => { }; LHNTestUtils.getDefaultRenderedSidebarLinks(draftReport.reportID); + const betas = [CONST.BETAS.VIOLATIONS]; + return ( waitForBatchedUpdates() // When Onyx is updated to contain that data and the sidebar re-renders .then(() => Onyx.multiSet({ + [ONYXKEYS.BETAS]: betas, [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, From e205c9d9d416107e2fe8d8e1a3ca500c35d61b40 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 24 Nov 2023 12:48:21 +0100 Subject: [PATCH 0031/1081] fix: few type issues --- .../LHNOptionsList/LHNOptionsList.tsx | 33 ++++--------------- src/components/LHNOptionsList/types.ts | 3 +- src/styles/StyleUtils.ts | 4 +-- 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 967525b0ff6a..d71fe1db535d 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,5 +1,6 @@ +import {FlashList} from '@shopify/flash-list'; import React, {useCallback} from 'react'; -import {FlatList, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import withCurrentReportID from '@components/withCurrentReportID'; import compose from '@libs/compose'; @@ -31,26 +32,6 @@ function LHNOptionsList({ }: LHNOptionsListProps) { const styles = useThemeStyles(); - /** - * This function is used to compute the layout of any given item in our list. Since we know that each item will have the exact same height, this is a performance optimization - * so that the heights can be determined before the options are rendered. Otherwise, the heights are determined when each option is rendering and it causes a lot of overhead on large - * lists. - * - * @param itemData - This is the same as the data we pass into the component - * @param index the current item's index in the set of data - */ - const getItemLayout = useCallback( - // eslint-disable-next-line @typescript-eslint/naming-convention - (_, index: number) => { - const optionHeight = optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight; - return { - length: optionHeight, - offset: index * optionHeight, - index, - }; - }, - [optionMode], - ); /** * Function which renders a row in the list */ @@ -88,19 +69,17 @@ function LHNOptionsList({ return ( - ); diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index fa0e48a413c4..bd6fb9a1a0a0 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -85,13 +85,12 @@ type OptionRowLHNDataProps = { type OptionRowLHNProps = { hoverStyle?: StyleProp; - betas?: Beta[]; reportID: string; isFocused?: boolean; onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; viewMode?: ValueOf; style?: ViewStyle | ViewStyle[]; - optionItem?: OptionData; + optionItem?: OptionData | null; }; export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps}; diff --git a/src/styles/StyleUtils.ts b/src/styles/StyleUtils.ts index 4b998f940244..695570f2669f 100644 --- a/src/styles/StyleUtils.ts +++ b/src/styles/StyleUtils.ts @@ -425,7 +425,7 @@ function getAutoGrowHeightInputStyle(textInputHeight: number, maxHeight: number) /** * Returns a style with backgroundColor and borderColor set to the same color */ -function getBackgroundAndBorderStyle(backgroundColor: string): ViewStyle { +function getBackgroundAndBorderStyle(backgroundColor: ColorValue): ViewStyle { return { backgroundColor, borderColor: backgroundColor, @@ -435,7 +435,7 @@ function getBackgroundAndBorderStyle(backgroundColor: string): ViewStyle { /** * Returns a style with the specified backgroundColor */ -function getBackgroundColorStyle(backgroundColor: string): ViewStyle { +function getBackgroundColorStyle(backgroundColor: ColorValue): ViewStyle { return { backgroundColor, }; From 658038c9865fee0d2a630c13242b118935168433 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Fri, 24 Nov 2023 23:10:18 +0530 Subject: [PATCH 0032/1081] fix: refactor utility methods --- src/libs/ComposerUtils/index.ts | 33 ++++++++++++++++++- .../ComposerWithSuggestions.js | 20 ++++------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 32ebca9afee8..54af287a67b7 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -14,6 +14,17 @@ function insertText(text: string, selection: Selection, textToInsert: string): s return text.slice(0, selection.start) + textToInsert + text.slice(selection.end, text.length); } +/** + * Insert a white space at given index of text + * @param text - text that needs whitespace to be appended to + * @param index - index at which whitespace should be inserted + * @returns + */ + +function insertWhiteSpaceAtIndex(text: string, index: number) { + return `${text.slice(0, index)} ${text.slice(index)}`; +} + /** * Check whether we can skip trigger hotkeys on some specific devices. */ @@ -23,4 +34,24 @@ function canSkipTriggerHotkeys(isSmallScreenWidth: boolean, isKeyboardShown: boo return (isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen()) || isKeyboardShown; } -export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys}; +/** + * Finds the length of common suffix between two texts + * @param str1 - first string to compare + * @param str2 - next string to compare + * @returns number - Length of the common suffix + */ +function findCommonSuffixLength(str1: string, str2: string) { + let commonSuffixLength = 0; + const minLength = Math.min(str1.length, str2.length); + for (let i = 1; i <= minLength; i++) { + if (str1.charAt(str1.length - i) === str2.charAt(str2.length - i)) { + commonSuffixLength++; + } else { + break; + } + } + + return commonSuffixLength; +} + +export {getNumberOfLines, updateNumberOfLines, insertText, canSkipTriggerHotkeys, insertWhiteSpaceAtIndex, findCommonSuffixLength}; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 1425b872f7f1..8793f617b306 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -209,7 +209,6 @@ function ComposerWithSuggestions({ [], ); - /** * Find the newly added characters between the previous text and the new text based on the selection. * @@ -226,14 +225,6 @@ function ComposerWithSuggestions({ let endIndex = -1; let currentIndex = 0; - const getCommonSuffixLength=(str1, str2) =>{ - let i = 0; - while (str1[str1.length - 1 - i] === str2[str2.length - 1 - i]) { - i++; - } - return i; - } - // Find the first character mismatch with newText while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { currentIndex++; @@ -241,8 +232,7 @@ function ComposerWithSuggestions({ if (currentIndex < newText.length) { startIndex = currentIndex; - - const commonSuffixLength = getCommonSuffixLength(prevText, newText); + const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText); // if text is getting pasted over find length of common suffix and subtract it from new text length if (commonSuffixLength > 0 || selection.end - selection.start > 0) { endIndex = newText.length - commonSuffixLength; @@ -260,8 +250,6 @@ function ComposerWithSuggestions({ [selection.end, selection.start], ); - const insertWhiteSpace = (text, index) => `${text.slice(0, index)} ${text.slice(index)}`; - /** * Update the value of the comment in Onyx * @@ -273,7 +261,11 @@ function ComposerWithSuggestions({ raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); - const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? insertWhiteSpace(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); + const { + text: newComment, + emojis, + cursorPosition, + } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); if (!_.isEmpty(emojis)) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (!_.isEmpty(newEmojis)) { From 0d1e9df22c910fc394a8669a2f951c1bc09edf26 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 24 Nov 2023 11:36:09 -1000 Subject: [PATCH 0033/1081] Upgrade required idea --- src/CONST.ts | 2 + src/Expensify.js | 13 +++ src/ONYXKEYS.ts | 4 + .../ErrorBoundary/BaseErrorBoundary.tsx | 10 ++- src/components/MenuItem.js | 20 +++-- src/libs/HttpUtils.ts | 5 ++ src/libs/actions/AppUpdate.ts | 6 +- src/pages/ErrorPage/GenericErrorPage.js | 79 +++++++++++-------- src/pages/settings/AppDownloadLinks.js | 68 ++-------------- src/styles/styles.ts | 2 +- 10 files changed, 98 insertions(+), 111 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index f1364ebbb5bf..223de7283530 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -731,6 +731,7 @@ const CONST = { EXP_ERROR: 666, MANY_WRITES_ERROR: 665, UNABLE_TO_RETRY: 'unableToRetry', + UPGRADE_REQUIRED: 426, }, HTTP_STATUS: { // When Cloudflare throttles @@ -761,6 +762,7 @@ const CONST = { GATEWAY_TIMEOUT: 'Gateway Timeout', EXPENSIFY_SERVICE_INTERRUPTED: 'Expensify service interrupted', DUPLICATE_RECORD: 'A record already exists with this ID', + UPGRADE_REQUIRED: 'Upgrade Required', }, ERROR_TYPE: { SOCKET: 'Expensify\\Auth\\Error\\Socket', diff --git a/src/Expensify.js b/src/Expensify.js index 1b692f86a197..8050ed99665a 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -36,6 +36,7 @@ import Visibility from './libs/Visibility'; import ONYXKEYS from './ONYXKEYS'; import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu'; import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; +import CONST from './CONST'; Onyx.registerLogger(({level, message}) => { if (level === 'alert') { @@ -76,6 +77,9 @@ const propTypes = { /** Whether the app is waiting for the server's response to determine if a room is public */ isCheckingPublicRoom: PropTypes.bool, + /** True when the user must update to the latest minimum version of the app */ + upgradeRequired: PropTypes.bool, + ...withLocalizePropTypes, }; @@ -88,6 +92,7 @@ const defaultProps = { isSidebarLoaded: false, screenShareRequest: null, isCheckingPublicRoom: true, + upgradeRequired: false, }; const SplashScreenHiddenContext = React.createContext({}); @@ -201,6 +206,10 @@ function Expensify(props) { return null; } + if (props.upgradeRequired) { + throw new Error(CONST.ERROR.UPGRADE_REQUIRED); + } + return ( {shouldInit && ( @@ -261,6 +270,10 @@ export default compose( screenShareRequest: { key: ONYXKEYS.SCREEN_SHARE_REQUEST, }, + upgradeRequired: { + key: ONYXKEYS.UPGRADE_REQUIRED, + initWithStoredValues: false, + } }), )(Expensify); diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 75c284fb9546..b30a2808a24b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -234,6 +234,9 @@ const ONYXKEYS = { // Max width supported for HTML element MAX_CANVAS_WIDTH: 'maxCanvasWidth', + /** Indicates whether an forced upgrade is required */ + UPGRADE_REQUIRED: 'upgradeRequired', + /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -417,6 +420,7 @@ type OnyxValues = { [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; + [ONYXKEYS.UPGRADE_REQUIRED]: boolean; // Collections [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx index 2a6524d5a993..2ba78c5f8863 100644 --- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx +++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import {ErrorBoundary} from 'react-error-boundary'; import BootSplash from '@libs/BootSplash'; import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage'; @@ -11,15 +11,17 @@ import {BaseErrorBoundaryProps, LogError} from './types'; */ function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) { - const catchError = (error: Error, errorInfo: React.ErrorInfo) => { - logError(errorMessage, error, JSON.stringify(errorInfo)); + const [error, setError] = useState(() => new Error()); + const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => { + logError(errorMessage, errorObject, JSON.stringify(errorInfo)); // We hide the splash screen since the error might happened during app init BootSplash.hide(); + setError(errorObject); }; return ( } + fallback={} onError={catchError} > {children} diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 9883672976e8..56461f52cc2a 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -175,14 +175,18 @@ const MenuItem = React.forwardRef((props, ref) => { onPressIn={() => props.shouldBlockSelection && isSmallScreenWidth && DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} onPressOut={ControlSelection.unblock} onSecondaryInteraction={props.onSecondaryInteraction} - style={({pressed}) => [ - props.style, - !props.interactive && styles.cursorDefault, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), - (isHovered || pressed) && props.hoverAndPressStyle, - ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), - props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, - ]} + style={({pressed}) => { + const s = [ + props.style, + !props.interactive && styles.cursorDefault, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || isHovered, pressed, props.success, props.disabled, props.interactive), true), + (isHovered || pressed) && props.hoverAndPressStyle, + ...(_.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle]), + props.shouldGreyOutWhenDisabled && props.disabled && styles.buttonOpacityDisabled, + ]; + console.log({s, style: props.style, ws: props.wrapperStyle}); + return s; + }} disabled={props.disabled} ref={ref} role={CONST.ACCESSIBILITY_ROLE.MENUITEM} diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index 859c8624833c..d51fb38e0cee 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -7,6 +7,7 @@ import {RequestType} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; +import * as AppUpdate from './actions/AppUpdate'; let shouldFailAllRequests = false; let shouldForceOffline = false; @@ -103,6 +104,10 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form alert('Too many auth writes', message); } } + if (response.jsonCode === CONST.JSON_CODE.UPGRADE_REQUIRED) { + // Trigger a modal and disable the app as the user needs to upgrade to the latest minimum version to continue + AppUpdate.triggerUpgradeRequired(); + } return response as Promise; }); } diff --git a/src/libs/actions/AppUpdate.ts b/src/libs/actions/AppUpdate.ts index 29ee2a4547ab..8f2ce3ead102 100644 --- a/src/libs/actions/AppUpdate.ts +++ b/src/libs/actions/AppUpdate.ts @@ -9,4 +9,8 @@ function setIsAppInBeta(isBeta: boolean) { Onyx.set(ONYXKEYS.IS_BETA, isBeta); } -export {triggerUpdateAvailable, setIsAppInBeta}; +function triggerUpgradeRequired() { + Onyx.set(ONYXKEYS.UPGRADE_REQUIRED, true); +} + +export {triggerUpdateAvailable, setIsAppInBeta, triggerUpgradeRequired}; diff --git a/src/pages/ErrorPage/GenericErrorPage.js b/src/pages/ErrorPage/GenericErrorPage.js index 7b627a8e18d5..cdf2ff1cea63 100644 --- a/src/pages/ErrorPage/GenericErrorPage.js +++ b/src/pages/ErrorPage/GenericErrorPage.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import {useErrorBoundary} from 'react-error-boundary'; import {View} from 'react-native'; import LogoWordmark from '@assets/images/expensify-wordmark.svg'; @@ -15,17 +16,21 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; +import AppDownloadLinksView from '@pages/settings/AppDownloadLinksView'; import ErrorBodyText from './ErrorBodyText'; const propTypes = { + /** Error object handled by the boundary */ + error: PropTypes.instanceOf(Error).isRequired, + ...withLocalizePropTypes, }; -function GenericErrorPage({translate}) { +function GenericErrorPage({translate, error}) { const theme = useTheme(); const styles = useThemeStyles(); const {resetBoundary} = useErrorBoundary(); - + const upgradeRequired = error.message === CONST.ERROR.UPGRADE_REQUIRED; return ( {({paddingBottom}) => ( @@ -34,46 +39,50 @@ function GenericErrorPage({translate}) { - {translate('genericErrorPage.title')} - - - - - {`${translate('genericErrorPage.body.helpTextConcierge')} `} - - {CONST.EMAIL.CONCIERGE} - - - - - -