From 08337c3bc03cea5a74ee0aa22818826fb3f4b964 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 20 Sep 2023 15:31:39 +0700 Subject: [PATCH 0001/1299] 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: Mon, 2 Oct 2023 16:28:03 +0200 Subject: [PATCH 0002/1299] 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 0003/1299] 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 00a7c235b3c37b96f022ada724f3aa6a0e6f2087 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 23 Oct 2023 15:41:54 +0500 Subject: [PATCH 0004/1299] perf: memoize components --- src/components/AttachmentModal.js | 4 ++-- src/components/EmojiPicker/EmojiPickerButton.js | 4 ++-- src/components/ExceededCommentLength.js | 4 ++-- src/pages/home/report/ReportActionCompose/SendButton.js | 4 ++-- src/pages/home/report/ReportTypingIndicator.js | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 61b138747950..9cceaab5e63e 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -1,4 +1,4 @@ -import React, {useState, useCallback, useRef, useMemo} from 'react'; +import React, {useState, useCallback, useRef, useMemo, memo} from 'react'; import PropTypes from 'prop-types'; import {View, Animated, Keyboard} from 'react-native'; import Str from 'expensify-common/lib/str'; @@ -534,4 +534,4 @@ export default compose( key: ONYXKEYS.SESSION, }, }), -)(AttachmentModal); +)(memo(AttachmentModal)); diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index cbfc3517117c..549e7d75005e 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -1,4 +1,4 @@ -import React, {useEffect, useRef} from 'react'; +import React, {memo, useEffect, useRef} from 'react'; import PropTypes from 'prop-types'; import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; @@ -64,4 +64,4 @@ function EmojiPickerButton(props) { EmojiPickerButton.propTypes = propTypes; EmojiPickerButton.defaultProps = defaultProps; EmojiPickerButton.displayName = 'EmojiPickerButton'; -export default withLocalize(EmojiPickerButton); +export default withLocalize(memo(EmojiPickerButton)); diff --git a/src/components/ExceededCommentLength.js b/src/components/ExceededCommentLength.js index 9c785cec0395..587ab11cac37 100644 --- a/src/components/ExceededCommentLength.js +++ b/src/components/ExceededCommentLength.js @@ -1,4 +1,4 @@ -import React, {useEffect, useState, useMemo} from 'react'; +import React, {useEffect, useState, useMemo, memo} from 'react'; import PropTypes from 'prop-types'; import {debounce} from 'lodash'; import {withOnyx} from 'react-native-onyx'; @@ -65,4 +65,4 @@ export default withOnyx({ key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, initialValue: '', }, -})(ExceededCommentLength); +})(memo(ExceededCommentLength)); diff --git a/src/pages/home/report/ReportActionCompose/SendButton.js b/src/pages/home/report/ReportActionCompose/SendButton.js index a97dd420e181..0e1d10d44092 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.js +++ b/src/pages/home/report/ReportActionCompose/SendButton.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {memo} from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; import PropTypes from 'prop-types'; @@ -61,4 +61,4 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}) { SendButton.propTypes = propTypes; SendButton.displayName = 'SendButton'; -export default SendButton; +export default memo(SendButton); diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index db97f712d65f..b4e5bbe8fe9d 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React, {memo, useMemo} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; import {withOnyx} from 'react-native-onyx'; @@ -77,4 +77,4 @@ export default compose( initialValue: {}, }, }), -)(ReportTypingIndicator); +)(memo(ReportTypingIndicator)); From 9c4f2995a3a86c75ea17f039939199396049d379 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 23 Oct 2023 15:43:15 +0500 Subject: [PATCH 0005/1299] refactor: simplify props --- src/pages/home/ReportScreen.js | 50 ++++++++---- .../SilentCommentUpdater.js | 16 ++-- src/pages/home/report/ReportActionsList.js | 2 +- .../report/ReportActionsListItemRenderer.js | 9 +-- src/pages/home/report/ReportActionsView.js | 10 ++- src/pages/home/report/ReportFooter.js | 81 ++++++++++++------- 6 files changed, 107 insertions(+), 61 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 81000c2dab92..682edb701c14 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,10 +1,10 @@ -import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react'; +import React, {useRef, useState, useEffect, useMemo, useCallback, memo} from 'react'; import {withOnyx} from 'react-native-onyx'; import {useFocusEffect} from '@react-navigation/native'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import lodashGet from 'lodash/get'; -import _ from 'underscore'; +import _, {isEqual} from 'underscore'; import styles from '../../styles/styles'; import ScreenWrapper from '../../components/ScreenWrapper'; import HeaderView from './HeaderView'; @@ -163,8 +163,9 @@ function ReportScreen({ const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; + const isEmptyChat = useMemo(() => _.isEmpty(reportActions), [reportActions]); // There are no reportActions at all to display and we are still in the process of loading the next set of actions. - const isLoadingInitialReportActions = _.isEmpty(reportActions) && reportMetadata.isLoadingReportActions; + const isLoadingInitialReportActions = isEmptyChat && reportMetadata.isLoadingReportActions; const isOptimisticDelete = lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.CLOSED; @@ -173,6 +174,10 @@ function ReportScreen({ const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails); const parentReportAction = ReportActionsUtils.getParentReportAction(report); + const lastReportAction = useMemo( + () => _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)), + [reportActions, parentReportAction], + ); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] || {}; @@ -253,16 +258,6 @@ function ReportScreen({ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID)); }, [accountManagerReportID]); - /** - * @param {String} text - */ - const onSubmitComment = useCallback( - (text) => { - Report.addComment(getReportID(route), text); - }, - [route], - ); - useFocusEffect( useCallback(() => { const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => { @@ -441,14 +436,15 @@ function ReportScreen({ {isReportReadyForDisplay ? ( ) : ( @@ -515,4 +511,24 @@ export default compose( }, true, ), -)(ReportScreen); +)( + memo( + ReportScreen, + (prevProps, nextProps) => + prevProps.isSidebarLoaded === nextProps.isSidebarLoaded && + isEqual(prevProps.reportActions, nextProps.reportActions) && + isEqual(prevProps.reportMetadata, nextProps.reportMetadata) && + prevProps.isComposerFullSize === nextProps.isComposerFullSize && + isEqual(prevProps.betas, nextProps.betas) && + isEqual(prevProps.policies, nextProps.policies) && + prevProps.accountManagerReportID === nextProps.accountManagerReportID && + isEqual(prevProps.personalDetails, nextProps.personalDetails) && + prevProps.userLeavingStatus === nextProps.userLeavingStatus && + prevProps.report.reportID === nextProps.report.reportID && + prevProps.report.policyID === nextProps.report.policyID && + prevProps.report.isOptimisticReport === nextProps.report.isOptimisticReport && + prevProps.report.statusNum === nextProps.report.statusNum && + isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && + prevProps.currentReportID === nextProps.currentReportID, + ), +); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js index 09f9d368bdcc..2d7a65ca4cc7 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater.js @@ -9,12 +9,6 @@ const propTypes = { /** The comment of the report */ comment: PropTypes.string, - /** The report associated with the comment */ - report: PropTypes.shape({ - /** The ID of the report */ - reportID: PropTypes.string, - }).isRequired, - /** The value of the comment */ value: PropTypes.string.isRequired, @@ -26,6 +20,8 @@ const propTypes = { /** Updates the comment */ updateComment: PropTypes.func.isRequired, + + reportID: PropTypes.string.isRequired, }; const defaultProps = { @@ -38,9 +34,9 @@ const defaultProps = { * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. * @returns {null} */ -function SilentCommentUpdater({comment, commentRef, report, value, updateComment}) { +function SilentCommentUpdater({comment, commentRef, reportID, value, updateComment}) { const prevCommentProp = usePrevious(comment); - const prevReportId = usePrevious(report.reportID); + const prevReportId = usePrevious(reportID); const {preferredLocale} = useLocalize(); const prevPreferredLocale = usePrevious(preferredLocale); @@ -51,12 +47,12 @@ function SilentCommentUpdater({comment, commentRef, report, value, updateComment // As the report IDs change, make sure to update the composer comment as we need to make sure // we do not show incorrect data in there (ie. draft of message from other report). - if (preferredLocale === prevPreferredLocale && report.reportID === prevReportId && !shouldSyncComment) { + if (preferredLocale === prevPreferredLocale && reportID === prevReportId && !shouldSyncComment) { return; } updateComment(comment); - }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report.reportID, updateComment, value, commentRef]); + }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef]); return null; } diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index febcf3cd3507..a0cdee57f600 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -327,7 +327,7 @@ function ReportActionsList({ report={report} linkedReportActionID={linkedReportActionID} hasOutstandingIOU={hasOutstandingIOU} - sortedReportActions={sortedReportActions} + displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index 40b9ee9142b7..e8e1f630ceb6 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -22,9 +22,6 @@ const propTypes = { /** Whether the option has an outstanding IOU */ hasOutstandingIOU: PropTypes.bool, - /** Sorted actions prepared for display */ - sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)).isRequired, - /** The ID of the most recent IOU report action connected with the shown report */ mostRecentIOUReportActionID: PropTypes.string, @@ -36,6 +33,8 @@ const propTypes = { /** Linked report action ID */ linkedReportActionID: PropTypes.string, + + displayAsGroup: PropTypes.bool.isRequired, }; const defaultProps = { @@ -49,7 +48,7 @@ function ReportActionsListItemRenderer({ index, report, hasOutstandingIOU, - sortedReportActions, + displayAsGroup, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker, @@ -73,7 +72,7 @@ function ReportActionsListItemRenderer({ report={report} action={reportAction} linkedReportActionID={linkedReportActionID} - displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} + displayAsGroup={displayAsGroup} shouldDisplayNewMarker={shouldDisplayNewMarker} shouldShowSubscriptAvatar={ (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isExpenseReport(report)) && diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a3671faf194c..db9b6e1c3ecf 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -179,6 +179,14 @@ function ReportActionsView(props) { } }; + const report = useMemo( + () => ({ + lastReadTime: props.report.lastReadTime, + reportID: props.report.reportID, + }), + [props.report.lastReadTime, props.report.reportID], + ); + // Comments have not loaded at all yet do nothing if (!_.size(props.reportActions)) { return null; @@ -187,7 +195,7 @@ function ReportActionsView(props) { return ( <> {}, pendingAction: null, personalDetails: {}, - shouldShowComposeInput: true, shouldDisableCompose: false, listHeight: 0, isReportReadyForDisplay: true, + lastReportAction: null, + isEmptyChat: true, }; function ReportFooter(props) { @@ -72,6 +67,33 @@ function ReportFooter(props) { const isSmallSizeLayout = props.windowWidth - (props.isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = ReportUtils.shouldDisableWriteActions(props.report); + const [shouldShowComposeInput, setShouldShowComposeInput] = useState(false); + + useEffect(() => { + // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs + const connID = Onyx.connect({ + key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, + callback: (val) => { + if (val === shouldShowComposeInput) { + return; + } + setShouldShowComposeInput(val); + }, + }); + + return () => { + Onyx.disconnect(connID); + }; + }, [shouldShowComposeInput]); + + const onSubmitComment = useCallback( + (text) => { + Report.addComment(props.reportID, text); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + return ( <> {hideComposer && ( @@ -89,14 +111,14 @@ function ReportFooter(props) { )} )} - {!hideComposer && (props.shouldShowComposeInput || !props.isSmallScreenWidth) && ( + {!hideComposer && (shouldShowComposeInput || !props.isSmallScreenWidth) && ( + isEqual(prevProps.report, nextProps.report) && + isEqual(prevProps.reportActions, nextProps.reportActions) && + isEqual(prevProps.personalDetails, nextProps.personalDetails) && + prevProps.pendingAction === nextProps.pendingAction && + prevProps.shouldDisableCompose === nextProps.shouldDisableCompose && + prevProps.listHeight === nextProps.listHeight && + prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay, + ), +); From 75ad6135c5391135cdd39d1f6e2b23a77825e0eb Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 23 Oct 2023 15:43:42 +0500 Subject: [PATCH 0006/1299] perf: reduce re-renders --- .../ComposerWithSuggestions.js | 100 +++++++++++------- .../ReportActionCompose.js | 68 ++++-------- 2 files changed, 83 insertions(+), 85 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index e194d0870885..bb5109073cfd 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -1,9 +1,10 @@ -import React, {useEffect, useCallback, useState, useRef, useMemo, useImperativeHandle} from 'react'; +import React, {useEffect, useCallback, useState, useRef, useMemo, useImperativeHandle, memo} from 'react'; import {View, NativeModules, findNodeHandle} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {useIsFocused, useNavigation} from '@react-navigation/native'; +import {runOnJS, useAnimatedRef} from 'react-native-reanimated'; import styles from '../../../../styles/styles'; import themeColors from '../../../../styles/themes/default'; import Composer from '../../../../components/Composer'; @@ -23,7 +24,6 @@ import * as EmojiUtils from '../../../../libs/EmojiUtils'; import * as User from '../../../../libs/actions/User'; import * as ReportUtils from '../../../../libs/ReportUtils'; import * as SuggestionUtils from '../../../../libs/SuggestionUtils'; -import * as ReportActionsUtils from '../../../../libs/ReportActionsUtils'; import canFocusInputOnScreenFocus from '../../../../libs/canFocusInputOnScreenFocus'; import SilentCommentUpdater from './SilentCommentUpdater'; import Suggestions from './Suggestions'; @@ -36,6 +36,10 @@ import focusWithDelay from '../../../../libs/focusWithDelay'; import useDebounce from '../../../../hooks/useDebounce'; import updateMultilineInputRange from '../../../../libs/UpdateMultilineInputRange'; import * as InputFocus from '../../../../libs/actions/InputFocus'; +import SendButton from './SendButton'; +import updatePropsPaperWorklet from '../../../../libs/updatePropsPaperWorklet'; +import EmojiPickerButton from '../../../../components/EmojiPicker/EmojiPickerButton'; +import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; const {RNTextInputReset} = NativeModules; @@ -70,14 +74,14 @@ function ComposerWithSuggestions({ // Onyx modal, preferredSkinTone, - parentReportActions, numberOfLines, // HOCs isKeyboardShown, // Props: Report reportID, - report, - reportActions, + includeChronos, + isEmptyChat, + lastReportAction, // Focus onFocus, onBlur, @@ -92,14 +96,13 @@ function ComposerWithSuggestions({ disabled, isFullComposerAvailable, setIsFullComposerAvailable, - setIsCommentEmpty, + isSendDisabled, handleSendMessage, shouldShowComposeInput, measureParentContainer, listHeight, // Refs suggestionsRef, - animatedRef, forwardedRef, isNextModalWillOpenRef, editFocused, @@ -108,19 +111,21 @@ function ComposerWithSuggestions({ const isFocused = useIsFocused(); const navigation = useNavigation(); const emojisPresentBefore = useRef([]); + + const draftComment = getDraftComment(reportID) || ''; + const [isCommentEmpty, setIsCommentEmpty] = useState(() => !draftComment || !!draftComment.match(/^(\s)*$/)); + const animatedRef = useAnimatedRef(); const [value, setValue] = useState(() => { - const draft = getDraftComment(reportID) || ''; - if (draft) { - emojisPresentBefore.current = EmojiUtils.extractEmojis(draft); + if (draftComment) { + emojisPresentBefore.current = EmojiUtils.extractEmojis(draftComment); } - return draft; + return draftComment; }); const commentRef = useRef(value); - const {isSmallScreenWidth} = useWindowDimensions(); + const {isSmallScreenWidth, isMediumScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || isEmptyChat) && shouldShowComposeInput; const valueRef = useRef(value); @@ -202,6 +207,20 @@ function ComposerWithSuggestions({ [], ); + const sendMessage = useCallback(() => { + 'worklet'; + + if (isCommentEmpty) { + return; + } + + runOnJS(handleSendMessage)(); + const viewTag = animatedRef(); + const viewName = 'RCTMultilineTextInputView'; + const updates = {text: ''}; + updatePropsPaperWorklet(viewTag, viewName, updates); // clears native text input on the UI thread + }, [animatedRef, handleSendMessage, isCommentEmpty]); + /** * Update the value of the comment in Onyx * @@ -225,7 +244,13 @@ function ComposerWithSuggestions({ } } emojisPresentBefore.current = emojis; - setIsCommentEmpty(!!newComment.match(/^(\s)*$/)); + const isNewCommentEmpty = !!newComment.match(/^(\s)*$/); + const isPrevCommentEmpty = !!commentRef.current.match(/^(\s)*$/); + + /** Only update isCommentEmpty state if it's different from previous one */ + if (isNewCommentEmpty !== isPrevCommentEmpty) { + setIsCommentEmpty(isNewCommentEmpty); + } setValue(newComment); if (commentValue !== newComment) { const remainder = ComposerUtils.getCommonSuffixLength(commentValue, newComment); @@ -338,26 +363,20 @@ function ComposerWithSuggestions({ // Submit the form when Enter is pressed if (e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !e.shiftKey) { e.preventDefault(); - handleSendMessage(); + sendMessage(); } // Trigger the edit box for last sent message if ArrowUp is pressed and the comment is empty and Chronos is not in the participants const valueLength = valueRef.current.length; - if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !ReportUtils.chatIncludesChronos(report)) { + if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !includeChronos) { e.preventDefault(); - const parentReportActionID = lodashGet(report, 'parentReportActionID', ''); - const parentReportAction = lodashGet(parentReportActions, [parentReportActionID], {}); - const lastReportAction = _.find( - [...reportActions, parentReportAction], - (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action), - ); if (lastReportAction) { Report.saveReportActionDraft(reportID, lastReportAction, _.last(lastReportAction.message).html); } } }, - [isKeyboardShown, isSmallScreenWidth, parentReportActions, report, reportActions, reportID, handleSendMessage, suggestionsRef, valueRef], + [isSmallScreenWidth, isKeyboardShown, suggestionsRef, includeChronos, sendMessage, lastReportAction, reportID], ); const onSelectionChange = useCallback( @@ -561,6 +580,19 @@ function ComposerWithSuggestions({ }} onScroll={hideSuggestionMenu} /> + + {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( + replaceSelectionWithText(...args)} + emojiPickerID={reportID} + /> + )} + `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - canEvict: false, - initWithStoredValues: false, - }, }), )( - React.forwardRef((props, ref) => ( - - )), + memo( + React.forwardRef((props, ref) => ( + + )), + ), ); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index dd4d51653546..8796fd1c1b85 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -1,10 +1,10 @@ -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; -import {runOnJS, useAnimatedRef} from 'react-native-reanimated'; +import {runOnJS} from 'react-native-reanimated'; import {PortalHost} from '@gorhom/portal'; import styles from '../../../../styles/styles'; import ONYXKEYS from '../../../../ONYXKEYS'; @@ -21,23 +21,17 @@ import ParticipantLocalTime from '../ParticipantLocalTime'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../../components/withCurrentUserPersonalDetails'; import {withNetwork} from '../../../../components/OnyxProvider'; import * as User from '../../../../libs/actions/User'; -import EmojiPickerButton from '../../../../components/EmojiPicker/EmojiPickerButton'; -import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; import OfflineIndicator from '../../../../components/OfflineIndicator'; import ExceededCommentLength from '../../../../components/ExceededCommentLength'; import ReportDropUI from '../ReportDropUI'; import reportPropTypes from '../../../reportPropTypes'; import OfflineWithFeedback from '../../../../components/OfflineWithFeedback'; -import SendButton from './SendButton'; import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems'; import ComposerWithSuggestions from './ComposerWithSuggestions'; -import reportActionPropTypes from '../reportActionPropTypes'; import useLocalize from '../../../../hooks/useLocalize'; import getModalState from '../../../../libs/getModalState'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; import * as EmojiPickerActions from '../../../../libs/actions/EmojiPickerAction'; -import getDraftComment from '../../../../libs/ComposerUtils/getDraftComment'; -import updatePropsPaperWorklet from '../../../../libs/updatePropsPaperWorklet'; const propTypes = { /** A method to call when the form is submitted */ @@ -46,9 +40,6 @@ const propTypes = { /** The ID of the report actions will be created for */ reportID: PropTypes.string.isRequired, - /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), - /** Personal details of all the users */ personalDetails: PropTypes.objectOf(participantPropTypes), @@ -111,14 +102,14 @@ function ReportActionCompose({ personalDetails, report, reportID, - reportActions, + isEmptyChat, + lastReportAction, listHeight, shouldShowComposeInput, isReportReadyForDisplay, }) { const {translate} = useLocalize(); - const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); - const animatedRef = useAnimatedRef(); + const {isSmallScreenWidth} = useWindowDimensions(); const actionButtonRef = useRef(null); /** @@ -134,10 +125,6 @@ function ReportActionCompose({ * Updates the should clear state of the composer */ const [textInputShouldClear, setTextInputShouldClear] = useState(false); - const [isCommentEmpty, setIsCommentEmpty] = useState(() => { - const draftComment = getDraftComment(reportID); - return !draftComment || !!draftComment.match(/^(\s)*$/); - }); /** * Updates the visibility state of the menu @@ -164,7 +151,9 @@ function ReportActionCompose({ [personalDetails, report, currentUserPersonalDetails.accountID, isComposerFullSize], ); - const isBlockedFromConcierge = useMemo(() => ReportUtils.chatIncludesConcierge(report) && User.isBlockedFromConcierge(blockedFromConcierge), [report, blockedFromConcierge]); + const includesConcierge = useMemo(() => ReportUtils.chatIncludesConcierge({participantAccountIDs: report.participantAccountIDs}), [report.participantAccountIDs]); + const userBlockedFromConcierge = useMemo(() => User.isBlockedFromConcierge(blockedFromConcierge), [blockedFromConcierge]); + const isBlockedFromConcierge = useMemo(() => includesConcierge && userBlockedFromConcierge, [includesConcierge, userBlockedFromConcierge]); // If we are on a small width device then don't show last 3 items from conciergePlaceholderOptions const conciergePlaceholderRandomIndex = useMemo( @@ -175,8 +164,8 @@ function ReportActionCompose({ // Placeholder to display in the chat input. const inputPlaceholder = useMemo(() => { - if (ReportUtils.chatIncludesConcierge(report)) { - if (User.isBlockedFromConcierge(blockedFromConcierge)) { + if (includesConcierge) { + if (userBlockedFromConcierge) { return translate('reportActionCompose.blockedFromConcierge'); } @@ -184,7 +173,7 @@ function ReportActionCompose({ } return translate('reportActionCompose.writeSomething'); - }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); + }, [includesConcierge, translate, userBlockedFromConcierge, conciergePlaceholderRandomIndex]); const focus = () => { if (composerRef === null || composerRef.current === null) { @@ -322,24 +311,16 @@ function ReportActionCompose({ const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); - const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || disabled || hasExceededMaxCommentLength; + const isSendDisabled = isBlockedFromConcierge || disabled || hasExceededMaxCommentLength; const handleSendMessage = useCallback(() => { - 'worklet'; - if (isSendDisabled || !isReportReadyForDisplay) { return; } - const viewTag = animatedRef(); - const viewName = 'RCTMultilineTextInputView'; - const updates = {text: ''}; - // We are setting the isCommentEmpty flag to true so the status of it will be in sync of the native text input state - runOnJS(setIsCommentEmpty)(true); runOnJS(resetFullComposerSize)(); - updatePropsPaperWorklet(viewTag, viewName, updates); // clears native text input on the UI thread runOnJS(submitForm)(); - }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); + }, [isSendDisabled, resetFullComposerSize, submitForm, isReportReadyForDisplay]); return ( { @@ -427,18 +409,6 @@ function ReportActionCompose({ )} - {DeviceCapabilities.canUseTouchScreen() && isMediumScreenWidth ? null : ( - composerRef.current.replaceSelectionWithText(...args)} - emojiPickerID={report.reportID} - /> - )} - Date: Tue, 24 Oct 2023 17:04:11 +0500 Subject: [PATCH 0007/1299] refactor: make action prop lightweight --- .../report/ReportActionsListItemRenderer.js | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index e8e1f630ceb6..a7ec10a30eea 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {memo} from 'react'; +import React, {memo, useMemo} from 'react'; import _ from 'underscore'; import CONST from '../../../CONST'; import * as ReportActionsUtils from '../../../libs/ReportActionsUtils'; @@ -59,6 +59,41 @@ function ReportActionsListItemRenderer({ ReportUtils.isChatThread(report) && !ReportActionsUtils.isTransactionThread(ReportActionsUtils.getParentReportAction(report)); + const action = useMemo( + () => ({ + reportActionID: reportAction.reportActionID, + message: reportAction.message, + pendingAction: reportAction.pendingAction, + actionName: reportAction.actionName, + errors: reportAction.errors, + originalMessage: reportAction.originalMessage, + childCommenterCount: reportAction.childCommenterCount, + linkMetadata: reportAction.linkMetadata, + childReportID: reportAction.childReportID, + childLastVisibleActionCreated: reportAction.childLastVisibleActionCreated, + whisperedToAccountIDs: reportAction.whisperedToAccountIDs, + error: reportAction.error, + created: reportAction.created, + actorAccountID: reportAction.actorAccountID, + }), + [ + reportAction.actionName, + reportAction.childCommenterCount, + reportAction.childLastVisibleActionCreated, + reportAction.childReportID, + reportAction.created, + reportAction.error, + reportAction.errors, + reportAction.linkMetadata, + reportAction.message, + reportAction.originalMessage, + reportAction.pendingAction, + reportAction.reportActionID, + reportAction.whisperedToAccountIDs, + reportAction.actorAccountID, + ], + ); + return shouldDisplayParentAction ? ( Date: Wed, 25 Oct 2023 18:25:16 +0500 Subject: [PATCH 0008/1299] perf: add Interaction Manager --- src/libs/actions/OnyxUpdates.ts | 29 +++++++++++++++------------ src/libs/actions/PersistedRequests.ts | 19 ++++++++++-------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 39a20ae9362a..5f13d8133f16 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -1,5 +1,6 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; import {Merge} from 'type-fest'; +import {InteractionManager} from 'react-native'; import PusherUtils from '../PusherUtils'; import ONYXKEYS from '../../ONYXKEYS'; import * as QueuedOnyxUpdates from './QueuedOnyxUpdates'; @@ -61,19 +62,21 @@ function apply({lastUpdateID, type, request, response, updates}: Merge | undefined { console.debug(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, {request, response, updates}); - if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) < lastUpdateIDAppliedToClient) { - console.debug('[OnyxUpdateManager] Update received was older than current state, returning without applying the updates'); - return Promise.resolve(); - } - if (lastUpdateID && (lastUpdateIDAppliedToClient === null || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) { - Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Number(lastUpdateID)); - } - if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) { - return applyHTTPSOnyxUpdates(request, response); - } - if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) { - return applyPusherOnyxUpdates(updates); - } + InteractionManager.runAfterInteractions(() => { + if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) < lastUpdateIDAppliedToClient) { + console.debug('[OnyxUpdateManager] Update received was older than current state, returning without applying the updates'); + return Promise.resolve(); + } + if (lastUpdateID && (lastUpdateIDAppliedToClient === null || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) { + Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Number(lastUpdateID)); + } + if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) { + return applyHTTPSOnyxUpdates(request, response); + } + if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) { + return applyPusherOnyxUpdates(updates); + } + }); } /** diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index d9f4ed020109..1e1a147cecd3 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,5 +1,6 @@ import Onyx from 'react-native-onyx'; import isEqual from 'lodash/isEqual'; +import {InteractionManager} from 'react-native'; import ONYXKEYS from '../../ONYXKEYS'; import {Request} from '../../types/onyx'; @@ -33,14 +34,16 @@ function remove(requestToRemove: Request) { * We only remove the first matching request because the order of requests matters. * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended. */ - const requests = [...persistedRequests]; - const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); - if (index === -1) { - return; - } - requests.splice(index, 1); - persistedRequests = requests; - Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests); + InteractionManager.runAfterInteractions(() => { + const requests = [...persistedRequests]; + const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); + if (index === -1) { + return; + } + requests.splice(index, 1); + persistedRequests = requests; + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests); + }); } function update(oldRequestIndex: number, newRequest: Request) { From 60d3034b9c91aaeb31626282f89e61e587416cbd Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 25 Oct 2023 18:30:54 +0500 Subject: [PATCH 0009/1299] perf: reference bindings of functions --- .../ComposerWithSuggestions.js | 33 ++++++++++++++----- .../composerWithSuggestionsProps.js | 18 ---------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 4c3136ff6328..97435e510389 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -40,6 +40,7 @@ import SendButton from './SendButton'; import updatePropsPaperWorklet from '../../../../libs/updatePropsPaperWorklet'; import EmojiPickerButton from '../../../../components/EmojiPicker/EmojiPickerButton'; import * as DeviceCapabilities from '../../../../libs/DeviceCapabilities'; +import * as ReportActionsUtils from '../../../../libs/ReportActionsUtils'; const {RNTextInputReset} = NativeModules; @@ -540,6 +541,26 @@ function ComposerWithSuggestions({ [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); + const onLayout = useCallback( + (e) => { + const composerLayoutHeight = e.nativeEvent.layout.height; + if (composerHeight === composerLayoutHeight) { + return; + } + setComposerHeight(composerLayoutHeight); + }, + [composerHeight], + ); + + const onClear = useCallback(() => { + setTextInputShouldClear(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const onChangeText = useCallback((text) => { + updateComment(text, true); + }, []); + return ( <> @@ -551,7 +572,7 @@ function ComposerWithSuggestions({ textAlignVertical="top" placeholder={inputPlaceholder} placeholderTextColor={themeColors.placeholderText} - onChangeText={(commentValue) => updateComment(commentValue, true)} + onChangeText={onChangeText} onKeyPress={triggerHotkeyActions} style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.flex4]} maxLines={maxComposerLines} @@ -560,7 +581,7 @@ function ComposerWithSuggestions({ onClick={setShouldBlockSuggestionCalcToFalse} onPasteFile={displayFileInModal} shouldClear={textInputShouldClear} - onClear={() => setTextInputShouldClear(false)} + onClear={onClear} isDisabled={isBlockedFromConcierge || disabled} isReportActionCompose selection={selection} @@ -572,13 +593,7 @@ function ComposerWithSuggestions({ numberOfLines={numberOfLines} onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition - onLayout={(e) => { - const composerLayoutHeight = e.nativeEvent.layout.height; - if (composerHeight === composerLayoutHeight) { - return; - } - setComposerHeight(composerLayoutHeight); - }} + onLayout={onLayout} onScroll={hideSuggestionMenu} /> diff --git a/src/pages/home/report/ReportActionCompose/composerWithSuggestionsProps.js b/src/pages/home/report/ReportActionCompose/composerWithSuggestionsProps.js index 017b5aecb4ae..246399d334d6 100644 --- a/src/pages/home/report/ReportActionCompose/composerWithSuggestionsProps.js +++ b/src/pages/home/report/ReportActionCompose/composerWithSuggestionsProps.js @@ -1,5 +1,4 @@ import PropTypes from 'prop-types'; -import reportActionPropTypes from '../reportActionPropTypes'; import CONST from '../../../../CONST'; const propTypes = { @@ -18,20 +17,9 @@ const propTypes = { /** Whether the keyboard is open or not */ isKeyboardShown: PropTypes.bool.isRequired, - /** The actions from the parent report */ - parentReportActions: PropTypes.objectOf(PropTypes.shape(reportActionPropTypes)), - - /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), - /** The ID of the report */ reportID: PropTypes.string.isRequired, - /** The report currently being looked at */ - report: PropTypes.shape({ - parentReportID: PropTypes.string, - }).isRequired, - /** Callback when the input is focused */ onFocus: PropTypes.func.isRequired, @@ -68,9 +56,6 @@ const propTypes = { /** Function to set whether the full composer is available or not */ setIsFullComposerAvailable: PropTypes.func.isRequired, - /** Function to set whether the comment is empty or not */ - setIsCommentEmpty: PropTypes.func.isRequired, - /** A method to call when the form is submitted */ handleSendMessage: PropTypes.func.isRequired, @@ -97,9 +82,6 @@ const propTypes = { }), }).isRequired, - /** Ref for the animated view (text input) */ - animatedRef: PropTypes.func.isRequired, - /** Ref for the composer */ forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), From 851c61f1edc0291d022b3480d22778e97a492ce9 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 26 Oct 2023 15:04:22 +0500 Subject: [PATCH 0010/1299] perf: optimise rendering --- .../ComposerWithSuggestions.js | 5 ---- .../ReportActionCompose/SuggestionMention.js | 27 +++++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 97435e510389..c41589fe4b68 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -283,14 +283,9 @@ function ComposerWithSuggestions({ } }, [ - debouncedUpdateFrequentlyUsedEmojis, - preferredLocale, preferredSkinTone, reportID, - setIsCommentEmpty, suggestionsRef, - raiseIsScrollLikelyLayoutTriggered, - debouncedSaveReportComment, ], ); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 67d6ac0632eb..237b1e956966 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -1,7 +1,7 @@ import React, {useState, useCallback, useRef, useImperativeHandle, useEffect} from 'react'; import PropTypes from 'prop-types'; import _ from 'underscore'; -import {withOnyx} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import CONST from '../../../../CONST'; import useArrowKeyFocusManager from '../../../../hooks/useArrowKeyFocusManager'; import MentionSuggestions from '../../../../components/MentionSuggestions'; @@ -29,9 +29,6 @@ const defaultSuggestionsValues = { }; const propTypes = { - /** Personal details of all users */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** A ref to this component */ forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), @@ -39,17 +36,28 @@ const propTypes = { }; const defaultProps = { - personalDetails: {}, forwardedRef: null, }; +/** + * We only need the personalDetails once because as long as the + * user is in the ReportScreen, these details won't be changing, + * hence we don't have to use it with `withOnyx`. + */ +let allPersonalDetails = {}; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => { + allPersonalDetails = val; + }, +}); + function SuggestionMention({ value, setValue, selection, setSelection, isComposerFullSize, - personalDetails, updateComment, composerHeight, forwardedRef, @@ -57,6 +65,7 @@ function SuggestionMention({ measureParentContainer, isComposerFocused, }) { + const personalDetails = allPersonalDetails; const {translate} = useLocalize(); const previousValue = usePrevious(value); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); @@ -316,8 +325,4 @@ const SuggestionMentionWithRef = React.forwardRef((props, ref) => ( SuggestionMentionWithRef.displayName = 'SuggestionMentionWithRef'; -export default withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, -})(SuggestionMentionWithRef); +export default SuggestionMentionWithRef; From e613f4278dc5cdfb85cf79670f9f6f575c826918 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 26 Oct 2023 16:28:03 +0500 Subject: [PATCH 0011/1299] perf: optimise usage of personal detail list --- src/components/AnonymousReportFooter.js | 5 --- src/components/ArchivedReportFooter.js | 13 ++----- src/components/AvatarWithDisplayName.js | 8 +--- src/components/HeaderWithBackButton/index.js | 2 - .../LHNOptionsList/OptionRowLHNData.js | 39 +------------------ src/components/MoneyReportHeader.js | 6 +-- src/components/MoneyRequestHeader.js | 6 +-- .../withCurrentUserPersonalDetails.tsx | 9 +---- src/libs/OptionsListUtils.js | 5 ++- src/libs/PersonalDetailsUtils.js | 27 +++++++++++-- src/libs/ReportUtils.js | 19 +++++---- src/libs/SidebarUtils.js | 10 ++++- src/pages/home/HeaderView.js | 8 +--- src/pages/home/ReportScreen.js | 15 +------ .../ReportActionCompose.js | 17 ++------ src/pages/home/report/ReportActionItem.js | 8 ++-- .../home/report/ReportActionItemCreated.js | 9 +---- .../home/report/ReportActionItemSingle.js | 5 ++- src/pages/home/report/ReportActionsList.js | 11 +----- src/pages/home/report/ReportFooter.js | 6 --- 20 files changed, 72 insertions(+), 156 deletions(-) diff --git a/src/components/AnonymousReportFooter.js b/src/components/AnonymousReportFooter.js index 43933210dc0b..902a31f12ee3 100644 --- a/src/components/AnonymousReportFooter.js +++ b/src/components/AnonymousReportFooter.js @@ -16,16 +16,12 @@ const propTypes = { isSmallSizeLayout: PropTypes.bool, - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - ...withLocalizePropTypes, }; const defaultProps = { report: {}, isSmallSizeLayout: false, - personalDetails: {}, }; function AnonymousReportFooter(props) { @@ -34,7 +30,6 @@ function AnonymousReportFooter(props) { diff --git a/src/components/ArchivedReportFooter.js b/src/components/ArchivedReportFooter.js index 71d331b68db0..bca5f0b391c8 100644 --- a/src/components/ArchivedReportFooter.js +++ b/src/components/ArchivedReportFooter.js @@ -34,9 +34,6 @@ const propTypes = { /** The archived report */ report: reportPropTypes.isRequired, - /** Personal details of all users */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - ...withLocalizePropTypes, }; @@ -46,19 +43,18 @@ const defaultProps = { reason: CONST.REPORT.ARCHIVE_REASON.DEFAULT, }, }, - personalDetails: {}, }; function ArchivedReportFooter(props) { const archiveReason = lodashGet(props.reportClosedAction, 'originalMessage.reason', CONST.REPORT.ARCHIVE_REASON.DEFAULT); - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetails, [props.report.ownerAccountID, 'displayName']); + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [props.report.ownerAccountID, 'displayName']); let oldDisplayName; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { const newAccountID = props.reportClosedAction.originalMessage.newAccountID; const oldAccountID = props.reportClosedAction.originalMessage.oldAccountID; - displayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetails, [newAccountID, 'displayName']); - oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(props.personalDetails, [oldAccountID, 'displayName']); + displayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [newAccountID, 'displayName']); + oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [oldAccountID, 'displayName']); } const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT; @@ -92,9 +88,6 @@ ArchivedReportFooter.displayName = 'ArchivedReportFooter'; export default compose( withLocalize, withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, reportClosedAction: { key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, canEvict: false, diff --git a/src/components/AvatarWithDisplayName.js b/src/components/AvatarWithDisplayName.js index 03ae8f51bfb6..3a4ebee0aa42 100644 --- a/src/components/AvatarWithDisplayName.js +++ b/src/components/AvatarWithDisplayName.js @@ -37,9 +37,6 @@ const propTypes = { /** The size of the avatar */ size: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - /** Whether if it's an unauthenticated user */ isAnonymous: PropTypes.bool, @@ -50,7 +47,6 @@ const propTypes = { }; const defaultProps = { - personalDetails: {}, policy: {}, report: {}, isAnonymous: false, @@ -93,8 +89,8 @@ function AvatarWithDisplayName(props) { const subtitle = ReportUtils.getChatRoomSubtitle(props.report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(props.report) || ReportUtils.isMoneyRequest(props.report); - const icons = ReportUtils.getIcons(props.report, props.personalDetails, props.policy); - const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([props.report.ownerAccountID], props.personalDetails); + const icons = ReportUtils.getIcons(props.report, null, props.policy); + const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([props.report.ownerAccountID]); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(_.values(ownerPersonalDetails), false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(props.report); const isExpenseRequest = ReportUtils.isExpenseRequest(props.report); diff --git a/src/components/HeaderWithBackButton/index.js b/src/components/HeaderWithBackButton/index.js index 6a02ce02237d..8e5fed15ff7e 100755 --- a/src/components/HeaderWithBackButton/index.js +++ b/src/components/HeaderWithBackButton/index.js @@ -28,7 +28,6 @@ function HeaderWithBackButton({ onThreeDotsButtonPress = () => {}, report = null, policy = {}, - personalDetails = {}, shouldShowAvatarWithDisplay = false, shouldShowBackButton = true, shouldShowBorderBottom = false, @@ -85,7 +84,6 @@ function HeaderWithBackButton({ ) : ( diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index e93e3690138e..2ddf8a687cfd 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -9,11 +9,9 @@ import compose from '../../libs/compose'; import ONYXKEYS from '../../ONYXKEYS'; import OptionRowLHN, {propTypes as basePropTypes, defaultProps as baseDefaultProps} from './OptionRowLHN'; import * as Report from '../../libs/actions/Report'; -import * as UserUtils from '../../libs/UserUtils'; import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; import * as TransactionUtils from '../../libs/TransactionUtils'; -import participantPropTypes from '../participantPropTypes'; import CONST from '../../CONST'; import reportActionPropTypes from '../../pages/home/report/reportActionPropTypes'; @@ -21,9 +19,6 @@ 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, @@ -54,7 +49,6 @@ const propTypes = { const defaultProps = { isFocused: false, - personalDetails: {}, fullReport: {}, policy: {}, parentReportActions: {}, @@ -73,7 +67,6 @@ function OptionRowLHNData({ isFocused, fullReport, reportActions, - personalDetails, preferredLocale, comment, policy, @@ -97,7 +90,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, preferredLocale, policy, parentReportAction); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; } @@ -106,7 +99,7 @@ function OptionRowLHNData({ // Listen parentReportAction to update title of thread report when parentReportAction changed // Listen to transaction to update title of transaction report when transaction changed // eslint-disable-next-line react-hooks/exhaustive-deps - }, [fullReport, linkedTransaction, reportActions, personalDetails, preferredLocale, policy, parentReportAction, transaction]); + }, [fullReport, linkedTransaction, reportActions, preferredLocale, policy, parentReportAction, transaction]); useEffect(() => { if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { @@ -130,30 +123,6 @@ OptionRowLHNData.propTypes = propTypes; OptionRowLHNData.defaultProps = defaultProps; OptionRowLHNData.displayName = 'OptionRowLHNData'; -/** - * @param {Object} [personalDetails] - * @returns {Object|undefined} - */ -const personalDetailsSelector = (personalDetails) => - _.reduce( - personalDetails, - (finalPersonalDetails, personalData, accountID) => { - // It's OK to do param-reassignment in _.reduce() because we absolutely know the starting state of finalPersonalDetails - // eslint-disable-next-line no-param-reassign - finalPersonalDetails[accountID] = { - accountID: Number(accountID), - login: personalData.login, - displayName: personalData.displayName, - firstName: personalData.firstName, - status: personalData.status, - avatar: UserUtils.getAvatar(personalData.avatar, personalData.accountID), - fallbackIcon: personalData.fallbackIcon, - }; - return finalPersonalDetails; - }, - {}, - ); - /** * This component is rendered in a list. * On scroll we want to avoid that a item re-renders @@ -174,10 +143,6 @@ export default React.memo( key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, canEvict: false, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - selector: personalDetailsSelector, - }, preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, }, diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index ab0b77c21653..211acaf033ce 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -46,9 +46,6 @@ const propTypes = { /** The next step for the report */ nextStep: nextStepPropTypes, - /** Personal details so we can get the ones for the report participants */ - personalDetails: PropTypes.objectOf(participantPropTypes).isRequired, - /** Session info for the currently logged in user. */ session: PropTypes.shape({ /** Currently logged in user email */ @@ -67,7 +64,7 @@ const defaultProps = { policy: {}, }; -function MoneyReportHeader({session, personalDetails, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) { +function MoneyReportHeader({session, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) { const {translate} = useLocalize(); const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); @@ -102,7 +99,6 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt shouldShowPinButton={false} report={moneyRequestReport} policy={policy} - personalDetails={personalDetails} shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)} // Shows border if no buttons or next steps are showing below the header diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 086e1429baef..d9be47ad312f 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -35,9 +35,6 @@ const propTypes = { name: PropTypes.string, }), - /** Personal details so we can get the ones for the report participants */ - personalDetails: PropTypes.objectOf(participantPropTypes).isRequired, - /* Onyx Props */ /** Session info for the currently logged in user. */ session: PropTypes.shape({ @@ -65,7 +62,7 @@ const defaultProps = { policy: {}, }; -function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}) { +function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy}) { const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const moneyRequestReport = parentReport; @@ -125,7 +122,6 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, ownerEmail: lodashGet(parentReport, 'ownerEmail', null), }} policy={policy} - personalDetails={personalDetails} shouldShowBackButton={isSmallScreenWidth} onBackButtonPress={() => Navigation.goBack(ROUTES.HOME, false, true)} /> diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index e1472f280f17..fa81c658bc78 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -4,13 +4,11 @@ import getComponentDisplayName from '../libs/getComponentDisplayName'; import ONYXKEYS from '../ONYXKEYS'; import personalDetailsPropType from '../pages/personalDetailsPropType'; import type {PersonalDetails, Session} from '../types/onyx'; +import { getPersonalDetailsByAccountID } from '../libs/PersonalDetailsUtils'; type CurrentUserPersonalDetails = PersonalDetails | Record; type OnyxProps = { - /** Personal details of all the users, including current user */ - personalDetails: OnyxEntry>; - /** Session of the current user */ session: OnyxEntry; }; @@ -35,7 +33,7 @@ export default function ( ): ComponentType & RefAttributes, keyof OnyxProps>> { function WithCurrentUserPersonalDetails(props: Omit, ref: ForwardedRef) { const accountID = props.session?.accountID ?? 0; - const accountPersonalDetails = props.personalDetails?.[accountID]; + const accountPersonalDetails: PersonalDetails = getPersonalDetailsByAccountID(accountID); const currentUserPersonalDetails: CurrentUserPersonalDetails = useMemo( () => (accountPersonalDetails ? {...accountPersonalDetails, accountID} : {}), [accountPersonalDetails, accountID], @@ -55,9 +53,6 @@ export default function ( const withCurrentUserPersonalDetails = React.forwardRef(WithCurrentUserPersonalDetails); return withOnyx & RefAttributes, OnyxProps>({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, session: { key: ONYXKEYS.SESSION, }, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 79c480711c4d..4948c4e8ef90 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -152,11 +152,12 @@ function getAvatarsForAccountIDs(accountIDs, personalDetails, defaultValues = {} * Returns the personal details for an array of accountIDs * * @param {Array} accountIDs - * @param {Object} personalDetails + * @param {Object} passedPersonalDetails * @returns {Object} – keys of the object are emails, values are PersonalDetails objects. */ -function getPersonalDetailsForAccountIDs(accountIDs, personalDetails) { +function getPersonalDetailsForAccountIDs(accountIDs, passedPersonalDetails) { const personalDetailsForAccountIDs = {}; + const personalDetails = passedPersonalDetails || allPersonalDetails; if (!personalDetails) { return personalDetailsForAccountIDs; } diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 29c49427bc81..78066c6b1d9d 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -23,8 +23,7 @@ Onyx.connect({ * @returns {String} */ function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue) { - const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); - + const displayName = lodashGet(passedPersonalDetails || allPersonalDetails, pathToDisplayName); return displayName || defaultValue || Localize.translateLocal('common.hidden'); } @@ -177,4 +176,26 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } -export {getDisplayNameOrDefault, getPersonalDetailsByIDs, getAccountIDsByLogins, getLoginsByAccountIDs, getNewPersonalDetailsOnyxData, getFormattedAddress}; +function getPersonalDetailsByAccountID(accountID) { + return allPersonalDetails[accountID]; +} + +function getWhisperedToPersonalDetails(whisperedToAccountIDs) { + return _.filter(allPersonalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)); +} + +function isPersonalDetailsEmpty() { + return !personalDetails.length; +} + +export { + getDisplayNameOrDefault, + getPersonalDetailsByIDs, + getAccountIDsByLogins, + getLoginsByAccountIDs, + getNewPersonalDetailsOnyxData, + getFormattedAddress, + getPersonalDetailsByAccountID, + getWhisperedToPersonalDetails, + isPersonalDetailsEmpty, +}; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 79c1c500b837..74d744efb837 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -913,15 +913,13 @@ function getReportRecipientAccountIDs(report, currentLoginAccountID) { /** * Whether the time row should be shown for a report. - * @param {Array} personalDetails * @param {Object} report - * @param {Number} accountID * @return {Boolean} */ -function canShowReportRecipientLocalTime(personalDetails, report, accountID) { - const reportRecipientAccountIDs = getReportRecipientAccountIDs(report, accountID); +function canShowReportRecipientLocalTime(report) { + const reportRecipientAccountIDs = getReportRecipientAccountIDs(report, currentUserAccountID); const hasMultipleParticipants = reportRecipientAccountIDs.length > 1; - const reportRecipient = personalDetails[reportRecipientAccountIDs[0]]; + const reportRecipient = allPersonalDetails[reportRecipientAccountIDs[0]]; const reportRecipientTimezone = lodashGet(reportRecipient, 'timezone', CONST.DEFAULT_TIME_ZONE); const isReportParticipantValidated = lodashGet(reportRecipient, 'validated', false); return Boolean( @@ -977,13 +975,13 @@ function getWorkspaceAvatar(report) { * The Avatar sources can be URLs or Icon components according to the chat type. * * @param {Array} participants - * @param {Object} personalDetails + * @param {Object} passedPersonalDetails * @returns {Array<*>} */ -function getIconsForParticipants(participants, personalDetails) { +function getIconsForParticipants(participants, passedPersonalDetails) { const participantDetails = []; const participantsList = participants || []; - + const personalDetails = passedPersonalDetails || allPersonalDetails; for (let i = 0; i < participantsList.length; i++) { const accountID = participantsList[i]; const avatarSource = UserUtils.getAvatar(lodashGet(personalDetails, [accountID, 'avatar'], ''), accountID); @@ -1046,14 +1044,15 @@ function getWorkspaceIcon(report, policy = undefined) { * The Avatar sources can be URLs or Icon components according to the chat type. * * @param {Object} report - * @param {Object} personalDetails + * @param {Object} passedPersonalDetails * @param {*} [defaultIcon] * @param {String} [defaultName] * @param {Number} [defaultAccountID] * @param {Object} [policy] * @returns {Array<*>} */ -function getIcons(report, personalDetails, defaultIcon = null, defaultName = '', defaultAccountID = -1, policy = undefined) { +function getIcons(report, passedPersonalDetails, defaultIcon = null, defaultName = '', defaultAccountID = -1, policy = undefined) { + const personalDetails = passedPersonalDetails || allPersonalDetails; if (_.isEmpty(report)) { const fallbackIcon = { source: defaultIcon || Expensicons.FallbackAvatar, diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 6b9e6d10fd16..5d7dd5bc1ed7 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -55,6 +55,12 @@ Onyx.connect({ }, }); +let allPersonalDetails; +Onyx.connect({ + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + callback: (val) => (allPersonalDetails = val), +}); + let resolveSidebarIsReadyPromise; let sidebarIsReadyPromise = new Promise((resolve) => { @@ -218,16 +224,16 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p * * @param {Object} report * @param {Object} reportActions - * @param {Object} personalDetails * @param {String} preferredLocale * @param {Object} [policy] * @param {Object} parentReportAction * @returns {Object} */ -function getOptionData(report, reportActions, personalDetails, preferredLocale, policy, parentReportAction) { +function getOptionData(report, reportActions, preferredLocale, policy, parentReportAction) { // 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 // a null check here and return early. + const personalDetails = allPersonalDetails; if (!report || !personalDetails) { return; } diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index e88f6cd0b756..d36937b0f420 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -43,9 +43,6 @@ const propTypes = { /** The report currently being looked at */ report: reportPropTypes, - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - /** Onyx Props */ parentReport: reportPropTypes, @@ -62,7 +59,6 @@ const propTypes = { }; const defaultProps = { - personalDetails: {}, report: null, guideCalendarLink: null, parentReport: {}, @@ -73,7 +69,7 @@ const defaultProps = { function HeaderView(props) { const participants = lodashGet(props.report, 'participantAccountIDs', []); - const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails); + const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants); const isMultipleParticipant = participants.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant); const isChatThread = ReportUtils.isChatThread(props.report); @@ -167,7 +163,7 @@ function HeaderView(props) { const shouldShowSubscript = ReportUtils.shouldReportShowSubscript(props.report); const defaultSubscriptSize = ReportUtils.isExpenseRequest(props.report) ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; - const icons = ReportUtils.getIcons(reportHeaderData, props.personalDetails); + const icons = ReportUtils.getIcons(reportHeaderData); const brickRoadIndicator = ReportUtils.hasReportNameError(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; const shouldShowBorderBottom = !isTaskReport || !props.isSmallScreenWidth; const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(props.report); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 64641ab473fb..d6caff573d25 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -40,6 +40,7 @@ import usePrevious from '../../hooks/usePrevious'; import CONST from '../../CONST'; import withCurrentReportID, {withCurrentReportIDPropTypes, withCurrentReportIDDefaultProps} from '../../components/withCurrentReportID'; import reportWithoutHasDraftSelector from '../../libs/OnyxSelectors/reportWithoutHasDraftSelector'; +import { isPersonalDetailsEmpty } from '../../libs/PersonalDetailsUtils'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -86,9 +87,6 @@ const propTypes = { /** The account manager report ID */ accountManagerReportID: PropTypes.string, - /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - /** Onyx function that marks the component ready for hydration */ markReadyForHydration: PropTypes.func, @@ -115,7 +113,6 @@ const defaultProps = { policies: {}, accountManagerReportID: null, userLeavingStatus: false, - personalDetails: {}, markReadyForHydration: null, ...withCurrentReportIDDefaultProps, }; @@ -140,7 +137,6 @@ function ReportScreen({ reportMetadata, reportActions, accountManagerReportID, - personalDetails, markReadyForHydration, policies, isSidebarLoaded, @@ -173,7 +169,7 @@ function ReportScreen({ const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); - const isLoading = !reportID || !isSidebarLoaded || _.isEmpty(personalDetails); + const isLoading = !reportID || !isSidebarLoaded || isPersonalDetailsEmpty(); const parentReportAction = ReportActionsUtils.getParentReportAction(report); const lastReportAction = useMemo( @@ -191,7 +187,6 @@ function ReportScreen({ Navigation.goBack(ROUTES.HOME, false, true)} - personalDetails={personalDetails} report={report} /> ); @@ -201,7 +196,6 @@ function ReportScreen({ @@ -213,7 +207,6 @@ function ReportScreen({ @@ -445,7 +438,6 @@ function ReportScreen({ isComposerFullSize={isComposerFullSize} policies={policies} listHeight={listHeight} - personalDetails={personalDetails} isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} /> @@ -506,9 +498,6 @@ export default compose( key: ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID, initialValue: null, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, userLeavingStatus: { key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`, initialValue: false, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 2721329919c7..dc515c2495ff 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -16,7 +16,6 @@ import willBlurTextInputOnTapOutsideFunc from '../../../../libs/willBlurTextInpu import canFocusInputOnScreenFocus from '../../../../libs/canFocusInputOnScreenFocus'; import CONST from '../../../../CONST'; import * as ReportUtils from '../../../../libs/ReportUtils'; -import participantPropTypes from '../../../../components/participantPropTypes'; import ParticipantLocalTime from '../ParticipantLocalTime'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../../components/withCurrentUserPersonalDetails'; import {withNetwork} from '../../../../components/OnyxProvider'; @@ -33,6 +32,7 @@ import getModalState from '../../../../libs/getModalState'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; import * as EmojiPickerActions from '../../../../libs/actions/EmojiPickerAction'; import * as ReportActionsUtils from '../../../../libs/ReportActionsUtils'; +import {getPersonalDetailsByAccountID} from '../../../../libs/PersonalDetailsUtils'; const propTypes = { /** A method to call when the form is submitted */ @@ -41,9 +41,6 @@ const propTypes = { /** The ID of the report actions will be created for */ reportID: PropTypes.string.isRequired, - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - /** The report currently being looked at */ report: reportPropTypes, @@ -76,7 +73,6 @@ const propTypes = { const defaultProps = { report: {}, blockedFromConcierge: {}, - personalDetails: {}, preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, isComposerFullSize: false, pendingAction: null, @@ -100,7 +96,6 @@ function ReportActionCompose({ network, onSubmit, pendingAction, - personalDetails, report, reportID, isEmptyChat, @@ -147,10 +142,7 @@ function ReportActionCompose({ [currentUserPersonalDetails.accountID, report], ); - const shouldShowReportRecipientLocalTime = useMemo( - () => ReportUtils.canShowReportRecipientLocalTime(personalDetails, report, currentUserPersonalDetails.accountID) && !isComposerFullSize, - [personalDetails, report, currentUserPersonalDetails.accountID, isComposerFullSize], - ); + const shouldShowReportRecipientLocalTime = useMemo(() => ReportUtils.canShowReportRecipientLocalTime(report) && !isComposerFullSize, [report, isComposerFullSize]); const includesConcierge = useMemo(() => ReportUtils.chatIncludesConcierge({participantAccountIDs: report.participantAccountIDs}), [report.participantAccountIDs]); const userBlockedFromConcierge = useMemo(() => User.isBlockedFromConcierge(blockedFromConcierge), [blockedFromConcierge]); @@ -307,7 +299,7 @@ function ReportActionCompose({ ); const reportRecipientAcountIDs = ReportUtils.getReportRecipientAccountIDs(report, currentUserPersonalDetails.accountID); - const reportRecipient = personalDetails[reportRecipientAcountIDs[0]]; + const reportRecipient = getPersonalDetailsByAccountID(reportRecipientAcountIDs[0]); const shouldUseFocusedColor = !isBlockedFromConcierge && !disabled && isFocused; const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); @@ -443,9 +435,6 @@ export default compose( blockedFromConcierge: { key: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, shouldShowComposeInput: { key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, }, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 3afdb437a49a..2697d4b5551d 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -133,7 +133,6 @@ const defaultProps = { }; function ReportActionItem(props) { - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const [isContextMenuActive, setIsContextMenuActive] = useState(() => ReportActionContextMenu.isActiveReportAction(props.action.reportActionID)); const [isHidden, setIsHidden] = useState(false); const [moderationDecision, setModerationDecision] = useState(CONST.MODERATION.MODERATOR_DECISION_APPROVED); @@ -303,6 +302,7 @@ function ReportActionItem(props) { */ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => { let children; + const _submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( @@ -360,7 +360,7 @@ function ReportActionItem(props) { /> ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); @@ -506,7 +506,7 @@ function ReportActionItem(props) { numberOfReplies={numberOfThreadReplies} mostRecentReply={`${props.action.childLastVisibleActionCreated}`} isHovered={hovered} - icons={ReportUtils.getIconsForParticipants(oldestFourAccountIDs, personalDetails)} + icons={ReportUtils.getIconsForParticipants(oldestFourAccountIDs)} onSecondaryInteraction={showPopover} /> @@ -638,7 +638,7 @@ function ReportActionItem(props) { const isWhisper = whisperedToAccountIDs.length > 0; const isMultipleParticipant = whisperedToAccountIDs.length > 1; const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedToAccountIDs); - const whisperedToPersonalDetails = isWhisper ? _.filter(personalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)) : []; + const whisperedToPersonalDetails = isWhisper ? PersonalDetailsUtils.getWhisperedToPersonalDetails(whisperedToAccountIDs) : []; const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, selector: reportWithoutHasDraftSelector, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index fc189a3aef36..faa9b4c6fbbc 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -31,6 +31,7 @@ import ONYXKEYS from '../../../ONYXKEYS'; import Text from '../../../components/Text'; import Tooltip from '../../../components/Tooltip'; import DateUtils from '../../../libs/DateUtils'; +import { getPersonalDetailsByAccountID } from '../../../libs/PersonalDetailsUtils'; const propTypes = { /** All the data of the action */ @@ -83,8 +84,8 @@ const showWorkspaceDetails = (reportID) => { }; function ReportActionItemSingle(props) { - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; const actorAccountID = props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport ? props.iouReport.managerID : props.action.actorAccountID; + const personalDetails = getPersonalDetailsByAccountID(actorAccountID); let {displayName} = personalDetails[actorAccountID] || {}; const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID] || {}; let actorHint = (login || displayName || '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); @@ -122,7 +123,7 @@ function ReportActionItemSingle(props) { id: secondaryAccountId, }; } else if (!isWorkspaceActor) { - secondaryAvatar = ReportUtils.getIcons(props.report, {})[props.report.isOwnPolicyExpenseChat ? 0 : 1]; + secondaryAvatar = ReportUtils.getIcons(props.report)[props.report.isOwnPolicyExpenseChat ? 0 : 1]; } const icon = {source: avatarSource, type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR, name: primaryDisplayName, id: isWorkspaceActor ? '' : actorAccountID}; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 531a11ce7252..6515dc652bdc 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -6,8 +6,6 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import CONST from '../../../CONST'; import InvertedFlatList from '../../../components/InvertedFlatList'; -import {withPersonalDetails} from '../../../components/OnyxProvider'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import useNetwork from '../../../hooks/useNetwork'; import useLocalize from '../../../hooks/useLocalize'; @@ -66,17 +64,14 @@ const propTypes = { }), ...windowDimensionsPropTypes, - ...withCurrentUserPersonalDetailsPropTypes, }; const defaultProps = { - personalDetails: {}, onScroll: () => {}, mostRecentIOUReportActionID: '', isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, - ...withCurrentUserPersonalDetailsDefaultProps, }; const VERTICAL_OFFSET_THRESHOLD = 200; @@ -122,8 +117,6 @@ function ReportActionsList({ onScroll, mostRecentIOUReportActionID, isSmallScreenWidth, - personalDetailsList, - currentUserPersonalDetails, hasOutstandingIOU, loadNewerChats, loadOlderChats, @@ -350,7 +343,7 @@ function ReportActionsList({ // To notify there something changes we can use extraData prop to flatlist const extraData = [isSmallScreenWidth ? currentUnreadMarker : undefined, ReportUtils.isArchivedRoom(report)]; const hideComposer = ReportUtils.shouldDisableWriteActions(report); - const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(personalDetailsList, report, currentUserPersonalDetails.accountID) && !isComposerFullSize; + const shouldShowReportRecipientLocalTime = ReportUtils.canShowReportRecipientLocalTime(report) && !isComposerFullSize; const contentContainerStyle = useMemo( () => [styles.chatContentScrollView, isLoadingNewerReportActions ? styles.chatContentScrollViewWithHeaderLoader : {}], @@ -435,4 +428,4 @@ ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withWindowDimensions, withPersonalDetails(), withCurrentUserPersonalDetails)(ReportActionsList); +export default compose(withWindowDimensions)(ReportActionsList); diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index 5af46b14142d..710eed2b86df 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -33,9 +33,6 @@ const propTypes = { /** The pending action when we are adding a chat */ pendingAction: PropTypes.string, - /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), - /** Whether user interactions should be disabled */ shouldDisableCompose: PropTypes.bool, @@ -51,7 +48,6 @@ const propTypes = { const defaultProps = { report: {reportID: '0'}, pendingAction: null, - personalDetails: {}, shouldDisableCompose: false, listHeight: 0, isReportReadyForDisplay: true, @@ -111,7 +107,6 @@ function ReportFooter(props) { )} {isArchivedRoom && } @@ -151,7 +146,6 @@ export default withWindowDimensions( (prevProps, nextProps) => isEqual(prevProps.report, nextProps.report) && isEqual(prevProps.reportActions, nextProps.reportActions) && - isEqual(prevProps.personalDetails, nextProps.personalDetails) && prevProps.pendingAction === nextProps.pendingAction && prevProps.shouldDisableCompose === nextProps.shouldDisableCompose && prevProps.listHeight === nextProps.listHeight && From 47c4717c3ee8553a96b1bb8540df9d5779097f5f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 26 Oct 2023 16:54:08 +0500 Subject: [PATCH 0012/1299] fix: linting --- src/components/AnonymousReportFooter.js | 1 - src/components/ArchivedReportFooter.js | 1 - src/components/AvatarWithDisplayName.js | 1 - src/components/MoneyReportHeader.js | 1 - src/components/MoneyRequestHeader.js | 1 - src/pages/home/HeaderView.js | 1 - src/pages/home/ReportScreen.js | 5 ++--- .../ReportActionCompose/ReportActionCompose.js | 4 ++-- .../ReportActionCompose/SuggestionMention.js | 2 +- src/pages/home/report/ReportActionItem.js | 3 +-- src/pages/home/report/ReportActionItemCreated.js | 1 - src/pages/home/report/ReportActionItemSingle.js | 15 +++++++-------- src/pages/home/report/ReportActionsList.js | 4 ++-- src/pages/home/report/ReportFooter.js | 1 - 14 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/components/AnonymousReportFooter.js b/src/components/AnonymousReportFooter.js index 902a31f12ee3..51b9752294e9 100644 --- a/src/components/AnonymousReportFooter.js +++ b/src/components/AnonymousReportFooter.js @@ -8,7 +8,6 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize'; import reportPropTypes from '../pages/reportPropTypes'; import styles from '../styles/styles'; import * as Session from '../libs/actions/Session'; -import participantPropTypes from './participantPropTypes'; const propTypes = { /** The report currently being looked at */ diff --git a/src/components/ArchivedReportFooter.js b/src/components/ArchivedReportFooter.js index bca5f0b391c8..41cf0f65487e 100644 --- a/src/components/ArchivedReportFooter.js +++ b/src/components/ArchivedReportFooter.js @@ -7,7 +7,6 @@ import CONST from '../CONST'; import Banner from './Banner'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import compose from '../libs/compose'; -import personalDetailsPropType from '../pages/personalDetailsPropType'; import ONYXKEYS from '../ONYXKEYS'; import * as ReportUtils from '../libs/ReportUtils'; import reportPropTypes from '../pages/reportPropTypes'; diff --git a/src/components/AvatarWithDisplayName.js b/src/components/AvatarWithDisplayName.js index 3a4ebee0aa42..8623f6bba4bc 100644 --- a/src/components/AvatarWithDisplayName.js +++ b/src/components/AvatarWithDisplayName.js @@ -5,7 +5,6 @@ import PropTypes from 'prop-types'; import lodashGet from 'lodash/get'; import CONST from '../CONST'; import reportPropTypes from '../pages/reportPropTypes'; -import participantPropTypes from './participantPropTypes'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import styles from '../styles/styles'; diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 211acaf033ce..55a31c0a9bdc 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -8,7 +8,6 @@ import useLocalize from '../hooks/useLocalize'; import HeaderWithBackButton from './HeaderWithBackButton'; import iouReportPropTypes from '../pages/iouReportPropTypes'; import * as ReportUtils from '../libs/ReportUtils'; -import participantPropTypes from './participantPropTypes'; import styles from '../styles/styles'; import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; import compose from '../libs/compose'; diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index d9be47ad312f..c106e1aa9bfd 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -8,7 +8,6 @@ import iouReportPropTypes from '../pages/iouReportPropTypes'; import * as ReportUtils from '../libs/ReportUtils'; import compose from '../libs/compose'; import * as Expensicons from './Icon/Expensicons'; -import participantPropTypes from './participantPropTypes'; import styles from '../styles/styles'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index d36937b0f420..3f9776ca4946 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -19,7 +19,6 @@ import TaskHeaderActionButton from '../../components/TaskHeaderActionButton'; import Text from '../../components/Text'; import ThreeDotsMenu from '../../components/ThreeDotsMenu'; import Tooltip from '../../components/Tooltip'; -import participantPropTypes from '../../components/participantPropTypes'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions'; import * as OptionsListUtils from '../../libs/OptionsListUtils'; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d6caff573d25..3340c4ca5ee3 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -28,7 +28,6 @@ import reportMetadataPropTypes from '../reportMetadataPropTypes'; import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; import withViewportOffsetTop from '../../components/withViewportOffsetTop'; import * as ReportActionsUtils from '../../libs/ReportActionsUtils'; -import personalDetailsPropType from '../personalDetailsPropType'; import getIsReportFullyVisible from '../../libs/getIsReportFullyVisible'; import MoneyRequestHeader from '../../components/MoneyRequestHeader'; import MoneyReportHeader from '../../components/MoneyReportHeader'; @@ -40,7 +39,7 @@ import usePrevious from '../../hooks/usePrevious'; import CONST from '../../CONST'; import withCurrentReportID, {withCurrentReportIDPropTypes, withCurrentReportIDDefaultProps} from '../../components/withCurrentReportID'; import reportWithoutHasDraftSelector from '../../libs/OnyxSelectors/reportWithoutHasDraftSelector'; -import { isPersonalDetailsEmpty } from '../../libs/PersonalDetailsUtils'; +import * as PersonalDetailsUtils from '../../libs/PersonalDetailsUtils'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -169,7 +168,7 @@ function ReportScreen({ const shouldHideReport = !ReportUtils.canAccessReport(report, policies, betas); - const isLoading = !reportID || !isSidebarLoaded || isPersonalDetailsEmpty(); + const isLoading = !reportID || !isSidebarLoaded || PersonalDetailsUtils.isPersonalDetailsEmpty(); const parentReportAction = ReportActionsUtils.getParentReportAction(report); const lastReportAction = useMemo( diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index dc515c2495ff..fa7dfc8418fa 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -32,7 +32,7 @@ import getModalState from '../../../../libs/getModalState'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; import * as EmojiPickerActions from '../../../../libs/actions/EmojiPickerAction'; import * as ReportActionsUtils from '../../../../libs/ReportActionsUtils'; -import {getPersonalDetailsByAccountID} from '../../../../libs/PersonalDetailsUtils'; +import * as PersonalDetailsUtils from '../../../../libs/PersonalDetailsUtils'; const propTypes = { /** A method to call when the form is submitted */ @@ -299,7 +299,7 @@ function ReportActionCompose({ ); const reportRecipientAcountIDs = ReportUtils.getReportRecipientAccountIDs(report, currentUserPersonalDetails.accountID); - const reportRecipient = getPersonalDetailsByAccountID(reportRecipientAcountIDs[0]); + const reportRecipient = PersonalDetailsUtils.getPersonalDetailsByAccountID(reportRecipientAcountIDs[0]); const shouldUseFocusedColor = !isBlockedFromConcierge && !disabled && isFocused; const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 237b1e956966..0172bfe20038 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -11,7 +11,6 @@ import * as SuggestionsUtils from '../../../../libs/SuggestionUtils'; import useLocalize from '../../../../hooks/useLocalize'; import usePrevious from '../../../../hooks/usePrevious'; import ONYXKEYS from '../../../../ONYXKEYS'; -import personalDetailsPropType from '../../../personalDetailsPropType'; import * as SuggestionProps from './suggestionProps'; /** @@ -45,6 +44,7 @@ const defaultProps = { * hence we don't have to use it with `withOnyx`. */ let allPersonalDetails = {}; +// eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, callback: (val) => { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 2697d4b5551d..7c955b0a88e3 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -28,7 +28,7 @@ import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMe import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import * as ContextMenuActions from './ContextMenu/ContextMenuActions'; import * as EmojiPickerAction from '../../../libs/actions/EmojiPickerAction'; -import {usePersonalDetails, withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '../../../components/OnyxProvider'; +import {withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '../../../components/OnyxProvider'; import RenameAction from '../../../components/ReportActionItem/RenameAction'; import InlineSystemMessage from '../../../components/InlineSystemMessage'; import styles from '../../../styles/styles'; @@ -302,7 +302,6 @@ function ReportActionItem(props) { */ const renderItemContent = (hovered = false, isWhisper = false, hasErrors = false) => { let children; - const _submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [props.report.ownerAccountID, 'displayName'], props.report.ownerEmail); // Show the MoneyRequestPreview for when request was created, bill was split or money was sent if ( diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.js index 9b428bf90ea6..2e7bce92f053 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.js @@ -5,7 +5,6 @@ import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; import ONYXKEYS from '../../../ONYXKEYS'; import ReportWelcomeText from '../../../components/ReportWelcomeText'; -import participantPropTypes from '../../../components/participantPropTypes'; import * as ReportUtils from '../../../libs/ReportUtils'; import styles from '../../../styles/styles'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index faa9b4c6fbbc..5450b457e53f 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -13,7 +13,6 @@ import compose from '../../../libs/compose'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; -import {usePersonalDetails} from '../../../components/OnyxProvider'; import ControlSelection from '../../../libs/ControlSelection'; import * as ReportUtils from '../../../libs/ReportUtils'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; @@ -31,7 +30,7 @@ import ONYXKEYS from '../../../ONYXKEYS'; import Text from '../../../components/Text'; import Tooltip from '../../../components/Tooltip'; import DateUtils from '../../../libs/DateUtils'; -import { getPersonalDetailsByAccountID } from '../../../libs/PersonalDetailsUtils'; +import * as PersonalDetailsUtils from '../../../libs/PersonalDetailsUtils'; const propTypes = { /** All the data of the action */ @@ -85,22 +84,22 @@ const showWorkspaceDetails = (reportID) => { function ReportActionItemSingle(props) { const actorAccountID = props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport ? props.iouReport.managerID : props.action.actorAccountID; - const personalDetails = getPersonalDetailsByAccountID(actorAccountID); - let {displayName} = personalDetails[actorAccountID] || {}; - const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID] || {}; + const personalDetails = PersonalDetailsUtils.getPersonalDetailsByAccountID(actorAccountID); + let {displayName} = personalDetails || {}; + const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails || {}; let actorHint = (login || displayName || '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); const displayAllActors = useMemo(() => props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && props.iouReport, [props.action.actionName, props.iouReport]); const isWorkspaceActor = ReportUtils.isPolicyExpenseChat(props.report) && (!actorAccountID || displayAllActors); let avatarSource = UserUtils.getAvatar(avatar, actorAccountID); + const delegateDetails = PersonalDetailsUtils.getPersonalDetailsByAccountID(props.action.delegateAccountID); if (isWorkspaceActor) { displayName = ReportUtils.getPolicyName(props.report); actorHint = displayName; avatarSource = ReportUtils.getWorkspaceAvatar(props.report); - } else if (props.action.delegateAccountID && personalDetails[props.action.delegateAccountID]) { + } else if (props.action.delegateAccountID && delegateDetails) { // We replace the actor's email, name, and avatar with the Copilot manually for now. And only if we have their // details. This will be improved upon when the Copilot feature is implemented. - const delegateDetails = personalDetails[props.action.delegateAccountID]; const delegateDisplayName = delegateDetails.displayName; actorHint = `${delegateDisplayName} (${props.translate('reportAction.asCopilot')} ${displayName})`; displayName = actorHint; @@ -113,7 +112,7 @@ function ReportActionItemSingle(props) { if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the a user requests money back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID; - const secondaryUserDetails = personalDetails[secondaryAccountId] || {}; + const secondaryUserDetails = PersonalDetailsUtils.getPersonalDetailsByAccountID(secondaryAccountId); const secondaryDisplayName = lodashGet(secondaryUserDetails, 'displayName', ''); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 6515dc652bdc..8127e40a3aa2 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -13,7 +13,6 @@ import useReportScrollManager from '../../../hooks/useReportScrollManager'; import DateUtils from '../../../libs/DateUtils'; import * as ReportUtils from '../../../libs/ReportUtils'; import * as Report from '../../../libs/actions/Report'; -import compose from '../../../libs/compose'; import styles from '../../../styles/styles'; import variables from '../../../styles/variables'; import reportPropTypes from '../../reportPropTypes'; @@ -72,6 +71,7 @@ const defaultProps = { isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, + policy: {} }; const VERTICAL_OFFSET_THRESHOLD = 200; @@ -428,4 +428,4 @@ ReportActionsList.propTypes = propTypes; ReportActionsList.defaultProps = defaultProps; ReportActionsList.displayName = 'ReportActionsList'; -export default compose(withWindowDimensions)(ReportActionsList); +export default withWindowDimensions(ReportActionsList); diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index 710eed2b86df..760f67dbc310 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -18,7 +18,6 @@ import reportActionPropTypes from './reportActionPropTypes'; import reportPropTypes from '../../reportPropTypes'; import * as ReportUtils from '../../../libs/ReportUtils'; import * as Session from '../../../libs/actions/Session'; -import participantPropTypes from '../../../components/participantPropTypes'; import * as Report from '../../../libs/actions/Report'; import useReportScrollManager from '../../../hooks/useReportScrollManager'; From 9562e8ca37be9b0dd961d9e6f7ea84df753f277a Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 27 Oct 2023 14:44:54 +0500 Subject: [PATCH 0013/1299] refactor: remove not needed code --- src/libs/PersonalDetailsUtils.js | 16 +++++++++- src/libs/actions/OnyxUpdates.ts | 29 +++++++++---------- src/libs/actions/PersistedRequests.ts | 19 +++++------- src/pages/home/ReportScreen.js | 3 +- .../report/ReportActionsListItemRenderer.js | 4 +++ src/pages/home/report/ReportActionsView.js | 7 ++++- src/pages/home/report/ReportFooter.js | 5 ++-- 7 files changed, 49 insertions(+), 34 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index c2841b139422..acbdeca80a9d 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -176,14 +176,28 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } +/** + * Get personal detail for an accountID + * @param {String} accountID + * @returns {PersonalDetail} personal detail object + */ function getPersonalDetailsByAccountID(accountID) { - return allPersonalDetails[accountID]; + return allPersonalDetails ? allPersonalDetails[accountID] : {}; } +/** + * Get whispered personal details for array of accountIDs + * @param {Array} whisperedToAccountIDs + * @returns {PersonalDetails} personal details + */ function getWhisperedToPersonalDetails(whisperedToAccountIDs) { return _.filter(allPersonalDetails, (details) => _.includes(whisperedToAccountIDs, details.accountID)); } +/** + * Whether personal details is empty + * @returns {Boolean} true if personal details is empty + */ function isPersonalDetailsEmpty() { return !personalDetails.length; } diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 5f13d8133f16..39a20ae9362a 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -1,6 +1,5 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; import {Merge} from 'type-fest'; -import {InteractionManager} from 'react-native'; import PusherUtils from '../PusherUtils'; import ONYXKEYS from '../../ONYXKEYS'; import * as QueuedOnyxUpdates from './QueuedOnyxUpdates'; @@ -62,21 +61,19 @@ function apply({lastUpdateID, type, request, response, updates}: Merge | undefined { console.debug(`[OnyxUpdateManager] Applying update type: ${type} with lastUpdateID: ${lastUpdateID}`, {request, response, updates}); - InteractionManager.runAfterInteractions(() => { - if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) < lastUpdateIDAppliedToClient) { - console.debug('[OnyxUpdateManager] Update received was older than current state, returning without applying the updates'); - return Promise.resolve(); - } - if (lastUpdateID && (lastUpdateIDAppliedToClient === null || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) { - Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Number(lastUpdateID)); - } - if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) { - return applyHTTPSOnyxUpdates(request, response); - } - if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) { - return applyPusherOnyxUpdates(updates); - } - }); + if (lastUpdateID && lastUpdateIDAppliedToClient && Number(lastUpdateID) < lastUpdateIDAppliedToClient) { + console.debug('[OnyxUpdateManager] Update received was older than current state, returning without applying the updates'); + return Promise.resolve(); + } + if (lastUpdateID && (lastUpdateIDAppliedToClient === null || Number(lastUpdateID) > lastUpdateIDAppliedToClient)) { + Onyx.merge(ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, Number(lastUpdateID)); + } + if (type === CONST.ONYX_UPDATE_TYPES.HTTPS && request && response) { + return applyHTTPSOnyxUpdates(request, response); + } + if (type === CONST.ONYX_UPDATE_TYPES.PUSHER && updates) { + return applyPusherOnyxUpdates(updates); + } } /** diff --git a/src/libs/actions/PersistedRequests.ts b/src/libs/actions/PersistedRequests.ts index 1e1a147cecd3..d9f4ed020109 100644 --- a/src/libs/actions/PersistedRequests.ts +++ b/src/libs/actions/PersistedRequests.ts @@ -1,6 +1,5 @@ import Onyx from 'react-native-onyx'; import isEqual from 'lodash/isEqual'; -import {InteractionManager} from 'react-native'; import ONYXKEYS from '../../ONYXKEYS'; import {Request} from '../../types/onyx'; @@ -34,16 +33,14 @@ function remove(requestToRemove: Request) { * We only remove the first matching request because the order of requests matters. * If we were to remove all matching requests, we can end up with a final state that is different than what the user intended. */ - InteractionManager.runAfterInteractions(() => { - const requests = [...persistedRequests]; - const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); - if (index === -1) { - return; - } - requests.splice(index, 1); - persistedRequests = requests; - Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests); - }); + const requests = [...persistedRequests]; + const index = requests.findIndex((persistedRequest) => isEqual(persistedRequest, requestToRemove)); + if (index === -1) { + return; + } + requests.splice(index, 1); + persistedRequests = requests; + Onyx.set(ONYXKEYS.PERSISTED_REQUESTS, requests); } function update(oldRequestIndex: number, newRequest: Request) { diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 76be144150c3..ed1fd91b9834 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -172,7 +172,7 @@ function ReportScreen({ const parentReportAction = ReportActionsUtils.getParentReportAction(report); const lastReportAction = useMemo( - () => _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)), + () => reportActions.length ? _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)) : {}, [reportActions, parentReportAction], ); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); @@ -437,7 +437,6 @@ function ReportScreen({ ({ reportActionID: reportAction.reportActionID, diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 415b8b6fbd1b..f1bd30f1a8b9 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -216,12 +216,17 @@ function ReportActionsView(props) { } }; + /** + * Create a lightweight Report so as to keep the re-rendering as light as possible by + * passing in only the required props. + */ const report = useMemo( () => ({ lastReadTime: props.report.lastReadTime, reportID: props.report.reportID, + policyID: props.report.policyID, }), - [props.report.lastReadTime, props.report.reportID], + [props.report.lastReadTime, props.report.reportID, props.report.policyID], ); // Comments have not loaded at all yet do nothing diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index 760f67dbc310..6f061cb3d015 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -85,7 +85,7 @@ function ReportFooter(props) { const onSubmitComment = useCallback( (text) => { - Report.addComment(props.reportID, text); + Report.addComment(props.report.reportID, text); // We need to scroll to the bottom of the list after the comment is added const refID = setTimeout(() => { @@ -119,7 +119,7 @@ function ReportFooter(props) { isEqual(prevProps.report, nextProps.report) && - isEqual(prevProps.reportActions, nextProps.reportActions) && prevProps.pendingAction === nextProps.pendingAction && prevProps.shouldDisableCompose === nextProps.shouldDisableCompose && prevProps.listHeight === nextProps.listHeight && From ad1b337ebc433635371547a8c8c8bbeeff800618 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 27 Oct 2023 16:15:37 +0500 Subject: [PATCH 0014/1299] fix: linting and test --- src/libs/PersonalDetailsUtils.js | 2 +- .../report/ReportActionCompose/ComposerWithSuggestions.js | 2 ++ tests/unit/ReportActionItemSingleTest.js | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index acbdeca80a9d..965f1b927095 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -178,7 +178,7 @@ function getFormattedAddress(privatePersonalDetails) { /** * Get personal detail for an accountID - * @param {String} accountID + * @param {Number} accountID * @returns {PersonalDetail} personal detail object */ function getPersonalDetailsByAccountID(accountID) { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 79c53650f27a..115cf404e589 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -283,6 +283,7 @@ function ComposerWithSuggestions({ debouncedBroadcastUserIsTyping(reportID); } }, + // eslint-disable-next-line react-hooks/exhaustive-deps [ preferredSkinTone, reportID, @@ -555,6 +556,7 @@ function ComposerWithSuggestions({ const onChangeText = useCallback((text) => { updateComment(text, true); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( diff --git a/tests/unit/ReportActionItemSingleTest.js b/tests/unit/ReportActionItemSingleTest.js index d6b46eb55414..799f6363d778 100644 --- a/tests/unit/ReportActionItemSingleTest.js +++ b/tests/unit/ReportActionItemSingleTest.js @@ -1,5 +1,5 @@ import Onyx from 'react-native-onyx'; -import {cleanup, screen} from '@testing-library/react-native'; +import {cleanup, screen, waitFor} from '@testing-library/react-native'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; @@ -70,7 +70,9 @@ describe('ReportActionItemSingle', () => { const expectedSecondaryIconTestId = 'SvgDefaultAvatar_w Icon'; return setup().then(() => { - expect(screen.getByTestId(expectedSecondaryIconTestId)).toBeDefined(); + waitFor(() => { + expect(screen.getByTestId(expectedSecondaryIconTestId)).toBeDefined(); + }); }); }); From 9a27bede1cd7a13f7f61bc32afdc23e91113026b Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 27 Oct 2023 17:08:19 +0500 Subject: [PATCH 0015/1299] fix: failing unread indicator test --- tests/ui/UnreadIndicatorsTest.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index a9ffe258ac7f..34e2c0322cde 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -524,13 +524,12 @@ describe('Unread Indicators', () => { .then(() => { // Simulate the response from the server so that the comment can be deleted in this test lastReportAction = {...CollectionUtils.lastItem(reportActions)}; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + return Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { lastMessageText: lastReportAction.message[0].text, lastVisibleActionCreated: DateUtils.getDBTime(lastReportAction.timestamp), lastActorAccountID: lastReportAction.actorAccountID, reportID: REPORT_ID, }); - return waitForBatchedUpdates(); }) .then(() => { // Verify the chat preview text matches the last comment from the current user From 9f294367e0293535250ba3857e9b121dd0a215b4 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 27 Oct 2023 17:15:08 +0500 Subject: [PATCH 0016/1299] fix: prettier issues --- src/components/LHNOptionsList/OptionRowLHNData.js | 13 +------------ src/components/withCurrentUserPersonalDetails.tsx | 2 +- src/libs/PersonalDetailsUtils.js | 4 ++-- src/libs/SidebarUtils.ts | 2 +- src/pages/home/ReportScreen.js | 5 ++++- .../ReportActionCompose/ComposerWithSuggestions.js | 8 ++------ src/pages/home/report/ReportActionsList.js | 2 +- 7 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index 2ddf8a687cfd..89142febf679 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -63,18 +63,7 @@ const defaultProps = { * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({ - isFocused, - fullReport, - reportActions, - preferredLocale, - comment, - policy, - receiptTransactions, - parentReportActions, - transaction, - ...propsToForward -}) { +function OptionRowLHNData({isFocused, fullReport, reportActions, preferredLocale, comment, policy, receiptTransactions, parentReportActions, transaction, ...propsToForward}) { const reportID = propsToForward.reportID; const parentReportAction = parentReportActions[fullReport.parentReportActionID]; diff --git a/src/components/withCurrentUserPersonalDetails.tsx b/src/components/withCurrentUserPersonalDetails.tsx index fa81c658bc78..a54f3f08c8b8 100644 --- a/src/components/withCurrentUserPersonalDetails.tsx +++ b/src/components/withCurrentUserPersonalDetails.tsx @@ -4,7 +4,7 @@ import getComponentDisplayName from '../libs/getComponentDisplayName'; import ONYXKEYS from '../ONYXKEYS'; import personalDetailsPropType from '../pages/personalDetailsPropType'; import type {PersonalDetails, Session} from '../types/onyx'; -import { getPersonalDetailsByAccountID } from '../libs/PersonalDetailsUtils'; +import {getPersonalDetailsByAccountID} from '../libs/PersonalDetailsUtils'; type CurrentUserPersonalDetails = PersonalDetails | Record; diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 965f1b927095..930ba19ad77b 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -178,7 +178,7 @@ function getFormattedAddress(privatePersonalDetails) { /** * Get personal detail for an accountID - * @param {Number} accountID + * @param {Number} accountID * @returns {PersonalDetail} personal detail object */ function getPersonalDetailsByAccountID(accountID) { @@ -187,7 +187,7 @@ function getPersonalDetailsByAccountID(accountID) { /** * Get whispered personal details for array of accountIDs - * @param {Array} whisperedToAccountIDs + * @param {Array} whisperedToAccountIDs * @returns {PersonalDetails} personal details */ function getWhisperedToPersonalDetails(whisperedToAccountIDs) { diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 6db4c5aa8241..c71548a483db 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -1,5 +1,5 @@ /* eslint-disable rulesdir/prefer-underscore-method */ -import Onyx, { OnyxEntry } from 'react-native-onyx'; +import Onyx, {OnyxEntry} from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; import {ValueOf} from 'type-fest'; import ONYXKEYS from '../ONYXKEYS'; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index ed1fd91b9834..31763e064c1d 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -172,7 +172,10 @@ function ReportScreen({ const parentReportAction = ReportActionsUtils.getParentReportAction(report); const lastReportAction = useMemo( - () => reportActions.length ? _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)) : {}, + () => + reportActions.length + ? _.find([...reportActions, parentReportAction], (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action)) + : {}, [reportActions, parentReportAction], ); const isSingleTransactionView = ReportUtils.isMoneyRequest(report); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 115cf404e589..26e33a08417e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -284,11 +284,7 @@ function ComposerWithSuggestions({ } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [ - preferredSkinTone, - reportID, - suggestionsRef, - ], + [preferredSkinTone, reportID, suggestionsRef], ); /** @@ -556,7 +552,7 @@ function ComposerWithSuggestions({ const onChangeText = useCallback((text) => { updateComment(text, true); - // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 8127e40a3aa2..6945cf82d158 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -71,7 +71,7 @@ const defaultProps = { isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, - policy: {} + policy: {}, }; const VERTICAL_OFFSET_THRESHOLD = 200; From 352227f178903ec5efe723578c47a12b3ff2ed95 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 27 Oct 2023 19:08:09 +0500 Subject: [PATCH 0017/1299] fix: failing test --- src/pages/home/report/ReportActionsView.js | 70 ++++------------------ tests/ui/UnreadIndicatorsTest.js | 20 ++++--- 2 files changed, 25 insertions(+), 65 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index f1bd30f1a8b9..266ed6e2e346 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -116,7 +116,7 @@ function ReportActionsView(props) { // update ref with current network state prevNetworkRef.current = props.network; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.network, props.report, isReportFullyVisible]); + }, [props.network, isReportFullyVisible]); useEffect(() => { const prevIsSmallScreenWidth = prevIsSmallScreenWidthRef.current; @@ -130,7 +130,7 @@ function ReportActionsView(props) { // update ref with current state prevIsSmallScreenWidthRef.current = props.isSmallScreenWidth; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isSmallScreenWidth, props.report, props.reportActions, isReportFullyVisible]); + }, [props.isSmallScreenWidth, props.reportActions, isReportFullyVisible]); useEffect(() => { // Ensures subscription event succeeds when the report/workspace room is created optimistically. @@ -142,7 +142,7 @@ function ReportActionsView(props) { Report.subscribeToReportTypingEvents(reportID); didSubscribeToReportTypingEvents.current = true; } - }, [props.report, didSubscribeToReportTypingEvents, reportID]); + }, [props.report.pendingFields, didSubscribeToReportTypingEvents, reportID]); /** * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently @@ -225,8 +225,9 @@ function ReportActionsView(props) { lastReadTime: props.report.lastReadTime, reportID: props.report.reportID, policyID: props.report.policyID, + lastVisibleActionCreated: props.report.lastVisibleActionCreated, }), - [props.report.lastReadTime, props.report.reportID, props.report.policyID], + [props.report.lastReadTime, props.report.reportID, props.report.policyID, props.report.lastVisibleActionCreated], ); // Comments have not loaded at all yet do nothing @@ -262,14 +263,6 @@ function arePropsEqual(oldProps, newProps) { return false; } - if (!_.isEqual(oldProps.report.pendingFields, newProps.report.pendingFields)) { - return false; - } - - if (!_.isEqual(oldProps.report.errorFields, newProps.report.errorFields)) { - return false; - } - if (lodashGet(oldProps.network, 'isOffline') !== lodashGet(newProps.network, 'isOffline')) { return false; } @@ -286,26 +279,14 @@ function arePropsEqual(oldProps, newProps) { return false; } - if (oldProps.report.lastReadTime !== newProps.report.lastReadTime) { - return false; - } - if (newProps.isSmallScreenWidth !== oldProps.isSmallScreenWidth) { return false; } - if (lodashGet(newProps.report, 'hasOutstandingIOU') !== lodashGet(oldProps.report, 'hasOutstandingIOU')) { - return false; - } - if (newProps.isComposerFullSize !== oldProps.isComposerFullSize) { return false; } - if (lodashGet(newProps.report, 'statusNum') !== lodashGet(oldProps.report, 'statusNum') || lodashGet(newProps.report, 'stateNum') !== lodashGet(oldProps.report, 'stateNum')) { - return false; - } - if (lodashGet(newProps, 'policy.avatar') !== lodashGet(oldProps, 'policy.avatar')) { return false; } @@ -314,39 +295,14 @@ function arePropsEqual(oldProps, newProps) { return false; } - if (lodashGet(newProps, 'report.reportName') !== lodashGet(oldProps, 'report.reportName')) { - return false; - } - - if (lodashGet(newProps, 'report.description') !== lodashGet(oldProps, 'report.description')) { - return false; - } - - if (lodashGet(newProps, 'report.managerID') !== lodashGet(oldProps, 'report.managerID')) { - return false; - } - - if (lodashGet(newProps, 'report.managerEmail') !== lodashGet(oldProps, 'report.managerEmail')) { - return false; - } - - if (lodashGet(newProps, 'report.total') !== lodashGet(oldProps, 'report.total')) { - return false; - } - - if (lodashGet(newProps, 'report.nonReimbursableTotal') !== lodashGet(oldProps, 'report.nonReimbursableTotal')) { - return false; - } - - if (lodashGet(newProps, 'report.writeCapability') !== lodashGet(oldProps, 'report.writeCapability')) { - return false; - } - - if (lodashGet(newProps, 'report.participantAccountIDs', 0) !== lodashGet(oldProps, 'report.participantAccountIDs', 0)) { - return false; - } - - return _.isEqual(lodashGet(newProps.report, 'icons', []), lodashGet(oldProps.report, 'icons', [])); + return ( + oldProps.report.lastReadTime === newProps.report.lastReadTime && + oldProps.report.reportID === newProps.report.reportID && + oldProps.report.policyID === newProps.report.policyID && + oldProps.report.lastVisibleActionCreated === newProps.report.lastVisibleActionCreated && + oldProps.report.isOptimisticReport === newProps.report.isOptimisticReport && + _.isEqual(oldProps.report.pendingFields, newProps.report.pendingFields) + ); } const MemoizedReportActionsView = React.memo(ReportActionsView, arePropsEqual); diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 34e2c0322cde..0fdee8aa63f7 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -130,13 +130,15 @@ function signInAndGetAppWithUnreadChat() { // Render the App and sign in as a test user. render(); return waitForBatchedUpdatesWithAct() - .then(() => { - const hintText = Localize.translateLocal('loginForm.loginForm'); - const loginForm = screen.queryAllByLabelText(hintText); - expect(loginForm).toHaveLength(1); - - return TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); - }) + .then(() => + waitFor(() => { + const hintText = Localize.translateLocal('loginForm.loginForm'); + const loginForm = screen.queryAllByLabelText(hintText); + expect(loginForm).toHaveLength(1); + + return TestHelper.signInWithTestUser(USER_A_ACCOUNT_ID, USER_A_EMAIL, undefined, undefined, 'A'); + }), + ) .then(() => { User.subscribeToUserEvents(); return waitForBatchedUpdates(); @@ -524,12 +526,14 @@ describe('Unread Indicators', () => { .then(() => { // Simulate the response from the server so that the comment can be deleted in this test lastReportAction = {...CollectionUtils.lastItem(reportActions)}; - return Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { lastMessageText: lastReportAction.message[0].text, lastVisibleActionCreated: DateUtils.getDBTime(lastReportAction.timestamp), lastActorAccountID: lastReportAction.actorAccountID, reportID: REPORT_ID, }); + + return waitForBatchedUpdates(); }) .then(() => { // Verify the chat preview text matches the last comment from the current user From 3820a4561ad4464d85adfbbda493ff776c37f01f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 6 Nov 2023 17:13:07 +0500 Subject: [PATCH 0018/1299] fix: reassure failures reported --- src/pages/home/ReportScreen.js | 22 ------------------- .../home/report/ReportActionItemSingle.js | 2 +- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index ceeae027f522..a2d76192820c 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,4 +1,3 @@ -import {useFocusEffect} from '@react-navigation/core'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -20,13 +19,11 @@ import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; -import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Navigation from '@libs/Navigation/Navigation'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import Visibility from '@libs/Visibility'; import reportMetadataPropTypes from '@pages/reportMetadataPropTypes'; import reportPropTypes from '@pages/reportPropTypes'; import styles from '@styles/styles'; @@ -259,25 +256,6 @@ function ReportScreen({ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID)); }, [accountManagerReportID]); - useFocusEffect( - useCallback(() => { - const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => { - const isTopMostReportID = Navigation.getTopmostReportId() === getReportID(route); - // If the report is not fully visible (AKA on small screen devices and LHR is open) or the report is optimistic (AKA not yet created) - // we don't need to call openReport - if (!getIsReportFullyVisible(isTopMostReportID) || report.isOptimisticReport) { - return; - } - - Report.openReport(report.reportID); - }); - - return () => unsubscribeVisibilityListener(); - // The effect should run only on the first focus to attach listener - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []), - ); - useEffect(() => { fetchReportIfNeeded(); ComposerActions.setShouldShowComposeInput(true); diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 634db2793710..cab7011e88fe 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -112,7 +112,7 @@ function ReportActionItemSingle(props) { if (displayAllActors) { // The ownerAccountID and actorAccountID can be the same if the a user requests money back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID; - const secondaryUserDetails = PersonalDetailsUtils.getPersonalDetailsByAccountID(secondaryAccountId); + const secondaryUserDetails = PersonalDetailsUtils.getPersonalDetailsByAccountID(secondaryAccountId) || {}; const secondaryDisplayName = lodashGet(secondaryUserDetails, 'displayName', ''); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { From 639136ad63796d54e7c18af31b6cd769fcd4f9e4 Mon Sep 17 00:00:00 2001 From: Aswin S Date: Tue, 7 Nov 2023 10:38:53 +0530 Subject: [PATCH 0019/1299] 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 ddf53d3c22e832d8d1ebecff9d08d0626909d067 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 7 Nov 2023 13:49:08 +0500 Subject: [PATCH 0020/1299] fix: reported reassure issues --- src/components/AttachmentModal.js | 23 ++++++- src/pages/home/ReportScreen.js | 1 - .../ReportActionCompose.js | 62 ++++++++++--------- src/pages/home/report/ReportFooter.js | 50 ++++++--------- 4 files changed, 75 insertions(+), 61 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 7fe342127272..8de07139e022 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str'; import lodashExtend from 'lodash/extend'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -393,6 +393,14 @@ function AttachmentModal(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAttachmentReceipt, props.parentReport, props.parentReportActions, props.policy, props.transaction]); + useImperativeHandle( + props.forwardedRef, + () => ({ + displayFileInModal: validateAndDisplayFileToUpload, + }), + [validateAndDisplayFileToUpload], + ); + return ( <> ( + +)); + +AttachmentModalWithRef.displayName = 'AttachmentModalWithRef'; + export default compose( withWindowDimensions, withLocalize, @@ -539,4 +558,4 @@ export default compose( key: ONYXKEYS.SESSION, }, }), -)(memo(AttachmentModal)); +)(memo(AttachmentModalWithRef)); diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index a2d76192820c..506e635b2336 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -419,7 +419,6 @@ function ReportScreen({ report={report} pendingAction={addWorkspaceRoomOrChatPendingAction} isComposerFullSize={isComposerFullSize} - policies={policies} listHeight={listHeight} isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 420422534ba7..8b61aa0fa199 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -136,6 +136,7 @@ function ReportActionCompose({ const suggestionsRef = useRef(null); const composerRef = useRef(null); + const attachementModalRef = useRef(null); const reportParticipantIDs = useMemo( () => _.without(lodashGet(report, 'participantAccountIDs', []), currentUserPersonalDetails.accountID), @@ -316,6 +317,10 @@ function ReportActionCompose({ runOnJS(submitForm)(); }, [isSendDisabled, resetFullComposerSize, submitForm, isReportReadyForDisplay]); + const onDisplayFileInModal = () => { + attachementModalRef.current.displayFileInModal(); + }; + return ( @@ -339,6 +344,7 @@ function ReportActionCompose({ ]} > setIsAttachmentPreviewActive(true)} @@ -365,34 +371,6 @@ function ReportActionCompose({ onItemSelected={onItemSelected} actionButtonRef={actionButtonRef} /> - { if (isAttachmentPreviewActive) { @@ -405,6 +383,34 @@ function ReportActionCompose({ )} + { - // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs - const connID = Onyx.connect({ - key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, - callback: (val) => { - if (val === shouldShowComposeInput) { - return; - } - setShouldShowComposeInput(val); - }, - }); - - return () => { - Onyx.disconnect(connID); - }; - }, [shouldShowComposeInput]); - const onSubmitComment = useCallback( (text) => { Report.addComment(props.report.reportID, text); @@ -114,7 +95,7 @@ function ReportFooter(props) { )} )} - {!hideComposer && (shouldShowComposeInput || !props.isSmallScreenWidth) && ( + {!hideComposer && (props.shouldShowComposeInput || !props.isSmallScreenWidth) && ( @@ -139,14 +119,24 @@ ReportFooter.displayName = 'ReportFooter'; ReportFooter.propTypes = propTypes; ReportFooter.defaultProps = defaultProps; -export default withWindowDimensions( +export default compose( + withWindowDimensions, + withOnyx({ + shouldShowComposeInput: { + key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, + }, + }), +)( memo( ReportFooter, (prevProps, nextProps) => isEqual(prevProps.report, nextProps.report) && prevProps.pendingAction === nextProps.pendingAction && - prevProps.shouldDisableCompose === nextProps.shouldDisableCompose && prevProps.listHeight === nextProps.listHeight && + prevProps.isComposerFullSize === nextProps.isComposerFullSize && + prevProps.isEmptyChat === nextProps.isEmptyChat && + prevProps.lastReportAction === nextProps.lastReportAction && + prevProps.shouldShowComposeInput === nextProps.shouldShowComposeInput && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay, ), ); From 4d299d6af14a810b62784d8c4debcc8358294bc9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 16:24:43 +0700 Subject: [PATCH 0021/1299] 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 5e2f21f8b62472cb67715f5bcd5f26e9e2b70e0e Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 7 Nov 2023 14:26:07 +0500 Subject: [PATCH 0022/1299] fix: reported reassure issues --- src/components/LHNOptionsList/LHNOptionsList.js | 14 +------------- .../ComposerWithSuggestions.js | 2 +- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index 3986773aca87..2603c0128af3 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -4,10 +4,8 @@ import React, {useCallback} from 'react'; import {FlatList, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import participantPropTypes from '@components/participantPropTypes'; import withCurrentReportID, {withCurrentReportIDDefaultProps, withCurrentReportIDPropTypes} 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 styles from '@styles/styles'; @@ -56,9 +54,6 @@ const propTypes = { /** Indicates which locale the user currently has selected */ preferredLocale: PropTypes.string, - /** List of users' personal details */ - personalDetails: PropTypes.objectOf(participantPropTypes), - /** The transaction from the parent report action */ transactions: PropTypes.objectOf( PropTypes.shape({ @@ -78,7 +73,6 @@ const defaultProps = { reports: {}, policy: {}, preferredLocale: CONST.LOCALES.DEFAULT, - personalDetails: {}, transactions: {}, draftComments: {}, ...withCurrentReportIDDefaultProps, @@ -97,7 +91,6 @@ function LHNOptionsList({ reportActions, policy, preferredLocale, - personalDetails, transactions, draftComments, currentReportID, @@ -144,7 +137,6 @@ function LHNOptionsList({ '', )}`; const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || ''; - const participantPersonalDetailList = _.values(OptionsListUtils.getPersonalDetailsForAccountIDs(itemFullReport.participantAccountIDs, personalDetails)); return ( ); }, - [currentReportID, draftComments, onSelectRow, optionMode, personalDetails, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], + [currentReportID, draftComments, onSelectRow, optionMode, policy, preferredLocale, reportActions, reports, shouldDisableFocusOptions, transactions], ); return ( @@ -206,9 +197,6 @@ export default compose( preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, transactions: { key: ONYXKEYS.COLLECTION.TRANSACTION, }, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index cf772230d2bf..7c5d0cfe2f2a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -29,6 +29,7 @@ import * as SuggestionUtils from '@libs/SuggestionUtils'; import updateMultilineInputRange from '@libs/UpdateMultilineInputRange'; import updatePropsPaperWorklet from '@libs/updatePropsPaperWorklet'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; +import SendButton from '@pages/home/report/ReportActionCompose/SendButton'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; import containerComposeStyles from '@styles/containerComposeStyles'; @@ -40,7 +41,6 @@ import * as Report from '@userActions/Report'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import SendButton from '@pages/home/report/ReportActionCompose/SendButton'; import {defaultProps, propTypes} from './composerWithSuggestionsProps'; const {RNTextInputReset} = NativeModules; From e2cc597f44a726885355ec8ebd5923739d2408fc Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 7 Nov 2023 17:04:57 +0700 Subject: [PATCH 0023/1299] 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 0024/1299] 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 0025/1299] 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 0026/1299] 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 0027/1299] 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 0028/1299] 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 0029/1299] 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 0030/1299] 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 46c9d60399162fb2f2ff30ac5edd9760d2d0e159 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 13 Nov 2023 13:54:36 +0500 Subject: [PATCH 0031/1299] fix: pr review --- src/components/ArchivedReportFooter.js | 6 ++--- src/components/AttachmentModal.js | 20 ++++------------ src/components/AvatarWithDisplayName.js | 2 +- src/libs/OptionsListUtils.js | 11 +++++++-- src/libs/PersonalDetailsUtils.js | 15 +++++++++--- src/libs/ReportUtils.js | 6 ++--- src/libs/SidebarUtils.ts | 4 ++-- src/libs/actions/Task.js | 2 +- src/pages/ReportDetailsPage.js | 2 +- .../AttachmentPickerWithMenuItems.js | 3 ++- .../composerWithSuggestionsProps.js | 3 ++- .../ReportActionCompose/SuggestionMention.js | 24 ++++++------------- src/pages/home/report/ReportActionItem.js | 2 +- src/pages/home/report/ReportFooter.js | 2 ++ 14 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/components/ArchivedReportFooter.js b/src/components/ArchivedReportFooter.js index a17cdb92136d..8c790f106ae7 100644 --- a/src/components/ArchivedReportFooter.js +++ b/src/components/ArchivedReportFooter.js @@ -46,14 +46,14 @@ const defaultProps = { function ArchivedReportFooter(props) { const archiveReason = lodashGet(props.reportClosedAction, 'originalMessage.reason', CONST.REPORT.ARCHIVE_REASON.DEFAULT); - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [props.report.ownerAccountID, 'displayName']); + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault([props.report.ownerAccountID, 'displayName']); let oldDisplayName; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { const newAccountID = props.reportClosedAction.originalMessage.newAccountID; const oldAccountID = props.reportClosedAction.originalMessage.oldAccountID; - displayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [newAccountID, 'displayName']); - oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [oldAccountID, 'displayName']); + displayName = PersonalDetailsUtils.getDisplayNameOrDefault([newAccountID, 'displayName']); + oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault([oldAccountID, 'displayName']); } const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT; diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 24ac0421c5b2..57ed1ff3e131 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -2,7 +2,7 @@ import Str from 'expensify-common/lib/str'; import lodashExtend from 'lodash/extend'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import {Animated, Keyboard, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -110,7 +110,7 @@ const defaultProps = { isWorkspaceAvatar: false, }; -function AttachmentModal(props) { +const AttachmentModal = forwardRef((props, ref) => { const onModalHideCallbackRef = useRef(null); const [isModalOpen, setIsModalOpen] = useState(props.defaultOpen); const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); @@ -394,7 +394,7 @@ function AttachmentModal(props) { }, [isAttachmentReceipt, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]); useImperativeHandle( - props.forwardedRef, + ref, () => ({ displayFileInModal: validateAndDisplayFileToUpload, }), @@ -514,22 +514,12 @@ function AttachmentModal(props) { })} ); -} +}); AttachmentModal.propTypes = propTypes; AttachmentModal.defaultProps = defaultProps; AttachmentModal.displayName = 'AttachmentModal'; -const AttachmentModalWithRef = React.forwardRef((props, ref) => ( - -)); - -AttachmentModalWithRef.displayName = 'AttachmentModalWithRef'; - export default compose( withWindowDimensions, withLocalize, @@ -558,4 +548,4 @@ export default compose( key: ONYXKEYS.SESSION, }, }), -)(memo(AttachmentModalWithRef)); +)(memo(AttachmentModal)); diff --git a/src/components/AvatarWithDisplayName.js b/src/components/AvatarWithDisplayName.js index 4be31b7d1a45..194a9239fa57 100644 --- a/src/components/AvatarWithDisplayName.js +++ b/src/components/AvatarWithDisplayName.js @@ -88,7 +88,7 @@ function AvatarWithDisplayName(props) { const subtitle = ReportUtils.getChatRoomSubtitle(props.report); const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(props.report); const isMoneyRequestOrReport = ReportUtils.isMoneyRequestReport(props.report) || ReportUtils.isMoneyRequest(props.report); - const icons = ReportUtils.getIcons(props.report, null, props.policy); + const icons = ReportUtils.getIcons(props.report, null, '', -1, props.policy); const ownerPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs([props.report.ownerAccountID]); const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(_.values(ownerPersonalDetails), false); const shouldShowSubscriptAvatar = ReportUtils.shouldReportShowSubscript(props.report); diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 485bfbbbf08d..6d5588548636 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -529,7 +529,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && 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('displayName', lastActorDetails), policyName: ReportUtils.getPolicyName(report), }); } @@ -561,7 +561,14 @@ 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.icons = ReportUtils.getIcons( + report, + UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), + personalDetail.login, + personalDetail.accountID, + undefined, + personalDetails, + ); result.subtitle = subtitle; return result; diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 842c06bb3228..bbcfd53454a9 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -17,13 +17,13 @@ Onyx.connect({ }); /** - * @param {Object | Null} passedPersonalDetails * @param {Array | String} pathToDisplayName * @param {String} [defaultValue] optional default display name value + * @param {Object | Null} passedPersonalDetails optional default personal details object * @returns {String} */ -function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue = '') { - const displayName = lodashGet(passedPersonalDetails || allPersonalDetails, pathToDisplayName); +function getDisplayNameOrDefault(pathToDisplayName, defaultValue = '', passedPersonalDetails = allPersonalDetails) { + const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); return displayName || defaultValue || Localize.translateLocal('common.hidden'); } @@ -203,6 +203,14 @@ function isPersonalDetailsEmpty() { return !personalDetails.length; } +/** + * Get personal details object + * @returns {PersonalDetail} personal detail object + */ +function getPersonalDetails() { + return allPersonalDetails || {}; +} + export { getDisplayNameOrDefault, getPersonalDetailsByIDs, @@ -213,4 +221,5 @@ export { getPersonalDetailsByAccountID, getWhisperedToPersonalDetails, isPersonalDetailsEmpty, + getPersonalDetails, }; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 83cf62f6da04..38846dce5bc0 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1103,15 +1103,15 @@ function getWorkspaceIcon(report, policy = undefined) { * The Avatar sources can be URLs or Icon components according to the chat type. * * @param {Object} report - * @param {Object} passedPersonalDetails * @param {*} [defaultIcon] * @param {String} [defaultName] * @param {Number} [defaultAccountID] * @param {Object} [policy] + * @param {Object} passedPersonalDetails * @returns {Array<*>} */ -function getIcons(report, passedPersonalDetails, defaultIcon = null, defaultName = '', defaultAccountID = -1, policy = undefined) { - const personalDetails = passedPersonalDetails || allPersonalDetails; +function getIcons(report, defaultIcon = null, defaultName = '', defaultAccountID = -1, policy = undefined, passedPersonalDetails = allPersonalDetails) { + const personalDetails = passedPersonalDetails; if (_.isEmpty(report)) { const fallbackIcon = { source: defaultIcon || Expensicons.FallbackAvatar, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 97ae1d25cdd9..826db3995c43 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -425,7 +425,7 @@ function getOptionData( case CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED: { lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { policyName: ReportUtils.getPolicyName(report, false, policy), - displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: PersonalDetailsUtils.getDisplayNameOrDefault('displayName', '', lastActorDetails), }); break; } @@ -515,7 +515,7 @@ function getOptionData( result.subtitle = subtitle; result.participantsList = participantPersonalDetailList; - result.icons = ReportUtils.getIcons(report, personalDetails, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), '', -1, policy); + result.icons = ReportUtils.getIcons(report, UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), '', -1, policy, personalDetails); result.searchText = OptionsListUtils.getSearchText(report, reportName, participantPersonalDetailList, result.isChatRoom || result.isPolicyExpenseChat, result.isThread); result.displayNamesWithTooltips = displayNamesWithTooltips; diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index e884a4d7a6b3..6329552d67e2 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -699,7 +699,7 @@ function getShareDestination(reportID, reports, personalDetails) { subtitle = ReportUtils.getChatRoomSubtitle(report); } return { - icons: ReportUtils.getIcons(report, personalDetails, Expensicons.FallbackAvatar), + icons: ReportUtils.getIcons(report, Expensicons.FallbackAvatar, '', -1, undefined, personalDetails), displayName: ReportUtils.getReportName(report), subtitle, displayNamesWithTooltips, diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index de25fdc3a081..935074462966 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -163,7 +163,7 @@ function ReportDetailsPage(props) { return ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails), hasMultipleParticipants); }, [participants, props.personalDetails]); - const icons = useMemo(() => ReportUtils.getIcons(props.report, props.personalDetails, props.policies), [props.report, props.personalDetails, props.policies]); + const icons = useMemo(() => ReportUtils.getIcons(props.report, null, '', -1, props.policies, props.personalDetails), [props.report, props.personalDetails, props.policies]); const chatRoomSubtitleText = chatRoomSubtitle ? ( {}, + disabled: false, }; export {propTypes, defaultProps}; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 6d85a0c349de..513e161dda41 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -1,16 +1,15 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import Onyx from 'react-native-onyx'; import _ from 'underscore'; import * as Expensicons from '@components/Icon/Expensicons'; import MentionSuggestions from '@components/MentionSuggestions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import * as SuggestionProps from './suggestionProps'; /** @@ -38,20 +37,6 @@ const defaultProps = { forwardedRef: null, }; -/** - * We only need the personalDetails once because as long as the - * user is in the ReportScreen, these details won't be changing, - * hence we don't have to use it with `withOnyx`. - */ -let allPersonalDetails = {}; -// eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - allPersonalDetails = val; - }, -}); - function SuggestionMention({ value, setValue, @@ -65,7 +50,12 @@ function SuggestionMention({ measureParentContainer, isComposerFocused, }) { - const personalDetails = allPersonalDetails; + /** + * We only need the personalDetails once because as long as the + * user is in the ReportScreen, these details won't be changing, + * hence we don't have to use it with `withOnyx`. + */ + const personalDetails = PersonalDetailsUtils.getPersonalDetails(); const {translate} = useLocalize(); const previousValue = usePrevious(value); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index fe150be8db90..6c056c1b2527 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -365,7 +365,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(null, [props.report.ownerAccountID, 'displayName']); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault([props.report.ownerAccountID, 'displayName']); const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index b407605a1c5d..d9490e17a688 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -137,6 +137,8 @@ export default compose( prevProps.isEmptyChat === nextProps.isEmptyChat && prevProps.lastReportAction === nextProps.lastReportAction && prevProps.shouldShowComposeInput === nextProps.shouldShowComposeInput && + prevProps.windowWidth === nextProps.windowWidth && + prevProps.isSmallScreenWidth === nextProps.isSmallScreenWidth && prevProps.isReportReadyForDisplay === nextProps.isReportReadyForDisplay, ), ); From 30f7f4b450129cbd6936c72ff69dd154dfdf9a19 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 15 Nov 2023 21:36:28 +0700 Subject: [PATCH 0032/1299] 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 0033/1299] 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 83d823ef9a37a5214319e54a41733d61061928ec Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 16 Nov 2023 12:16:32 +0500 Subject: [PATCH 0034/1299] fix: add default value --- src/libs/OptionsListUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index fc306a1f31bf..37b2e0add4ab 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -514,7 +514,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault('displayName', lastActorDetails), + displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault('displayName', '', lastActorDetails), policyName: ReportUtils.getPolicyName(report), }); } From 3e5c8b094bdec621983d5d075719ba2903bc031f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 20 Nov 2023 17:17:26 +0100 Subject: [PATCH 0035/1299] 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 0036/1299] 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 0037/1299] 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 0038/1299] 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 2e6f52a91aa81c4c8ab11703f8f20fc29770f0e9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 21 Nov 2023 20:21:41 +0100 Subject: [PATCH 0039/1299] 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 0040/1299] 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 2bf40c8f31db5342889c0b2845ec63f87c7a95ec Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 22 Nov 2023 19:37:24 +0100 Subject: [PATCH 0041/1299] 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 e205c9d9d416107e2fe8d8e1a3ca500c35d61b40 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 24 Nov 2023 12:48:21 +0100 Subject: [PATCH 0042/1299] 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 0043/1299] 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 0044/1299] 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} - - - - - -